@useconductor/conductor 1.0.0 → 1.0.1

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 (145) hide show
  1. package/.github/README.md +374 -7
  2. package/.github/workflows/ci.yml +3 -1
  3. package/.github/workflows/claude-code-review.yml +1 -15
  4. package/.github/workflows/publish.yml +43 -0
  5. package/README.md +290 -121
  6. package/dist/cli/commands/audit.d.ts +40 -0
  7. package/dist/cli/commands/audit.d.ts.map +1 -0
  8. package/dist/cli/commands/audit.js +272 -0
  9. package/dist/cli/commands/audit.js.map +1 -0
  10. package/dist/cli/commands/circuit.d.ts +13 -0
  11. package/dist/cli/commands/circuit.d.ts.map +1 -0
  12. package/dist/cli/commands/circuit.js +53 -0
  13. package/dist/cli/commands/circuit.js.map +1 -0
  14. package/dist/cli/commands/config.d.ts +31 -0
  15. package/dist/cli/commands/config.d.ts.map +1 -0
  16. package/dist/cli/commands/config.js +152 -0
  17. package/dist/cli/commands/config.js.map +1 -0
  18. package/dist/cli/commands/init.d.ts +5 -8
  19. package/dist/cli/commands/init.d.ts.map +1 -1
  20. package/dist/cli/commands/init.js +86 -123
  21. package/dist/cli/commands/init.js.map +1 -1
  22. package/dist/cli/commands/marketplace.js +1 -1
  23. package/dist/cli/commands/onboard.d.ts.map +1 -1
  24. package/dist/cli/commands/onboard.js +33 -11
  25. package/dist/cli/commands/onboard.js.map +1 -1
  26. package/dist/cli/commands/release.d.ts.map +1 -1
  27. package/dist/cli/commands/release.js +1 -1
  28. package/dist/cli/commands/release.js.map +1 -1
  29. package/dist/cli/index.js +146 -10
  30. package/dist/cli/index.js.map +1 -1
  31. package/dist/core/audit.d.ts.map +1 -1
  32. package/dist/core/audit.js +5 -2
  33. package/dist/core/audit.js.map +1 -1
  34. package/dist/core/conductor.d.ts.map +1 -1
  35. package/dist/core/conductor.js +12 -0
  36. package/dist/core/conductor.js.map +1 -1
  37. package/dist/core/config.d.ts +3 -0
  38. package/dist/core/config.d.ts.map +1 -1
  39. package/dist/core/config.js +46 -2
  40. package/dist/core/config.js.map +1 -1
  41. package/dist/core/database.d.ts +3 -0
  42. package/dist/core/database.d.ts.map +1 -1
  43. package/dist/core/database.js +26 -0
  44. package/dist/core/database.js.map +1 -1
  45. package/dist/core/encryption.d.ts +34 -0
  46. package/dist/core/encryption.d.ts.map +1 -0
  47. package/dist/core/encryption.js +96 -0
  48. package/dist/core/encryption.js.map +1 -0
  49. package/dist/core/zero-config.d.ts.map +1 -1
  50. package/dist/core/zero-config.js +1 -4
  51. package/dist/core/zero-config.js.map +1 -1
  52. package/dist/dashboard/server.d.ts.map +1 -1
  53. package/dist/dashboard/server.js +112 -16
  54. package/dist/dashboard/server.js.map +1 -1
  55. package/dist/mcp/server.d.ts.map +1 -1
  56. package/dist/mcp/server.js +30 -2
  57. package/dist/mcp/server.js.map +1 -1
  58. package/dist/plugins/builtin/aws.d.ts +31 -0
  59. package/dist/plugins/builtin/aws.d.ts.map +1 -0
  60. package/dist/plugins/builtin/aws.js +149 -0
  61. package/dist/plugins/builtin/aws.js.map +1 -0
  62. package/dist/plugins/builtin/database.d.ts +1 -0
  63. package/dist/plugins/builtin/database.d.ts.map +1 -1
  64. package/dist/plugins/builtin/database.js +26 -1
  65. package/dist/plugins/builtin/database.js.map +1 -1
  66. package/dist/plugins/builtin/docker.d.ts +4 -0
  67. package/dist/plugins/builtin/docker.d.ts.map +1 -1
  68. package/dist/plugins/builtin/docker.js +20 -1
  69. package/dist/plugins/builtin/docker.js.map +1 -1
  70. package/dist/plugins/builtin/gcp.d.ts +28 -0
  71. package/dist/plugins/builtin/gcp.d.ts.map +1 -0
  72. package/dist/plugins/builtin/gcp.js +135 -0
  73. package/dist/plugins/builtin/gcp.js.map +1 -0
  74. package/dist/plugins/builtin/index.d.ts.map +1 -1
  75. package/dist/plugins/builtin/index.js +4 -0
  76. package/dist/plugins/builtin/index.js.map +1 -1
  77. package/dist/plugins/builtin/jira.d.ts.map +1 -1
  78. package/dist/plugins/builtin/jira.js +4 -2
  79. package/dist/plugins/builtin/jira.js.map +1 -1
  80. package/dist/plugins/builtin/linear.js +1 -1
  81. package/dist/plugins/builtin/linear.js.map +1 -1
  82. package/dist/plugins/builtin/shell.js +1 -1
  83. package/dist/plugins/builtin/shell.js.map +1 -1
  84. package/dist/plugins/builtin/slack.d.ts +1 -0
  85. package/dist/plugins/builtin/slack.d.ts.map +1 -1
  86. package/dist/plugins/builtin/slack.js +9 -1
  87. package/dist/plugins/builtin/slack.js.map +1 -1
  88. package/dist/plugins/builtin/spotify.js +1 -1
  89. package/dist/plugins/builtin/spotify.js.map +1 -1
  90. package/dist/plugins/builtin/vercel.d.ts.map +1 -1
  91. package/dist/plugins/builtin/vercel.js +3 -1
  92. package/dist/plugins/builtin/vercel.js.map +1 -1
  93. package/dist/security/sso.d.ts +37 -0
  94. package/dist/security/sso.d.ts.map +1 -0
  95. package/dist/security/sso.js +92 -0
  96. package/dist/security/sso.js.map +1 -0
  97. package/docs/deployment.md +201 -0
  98. package/docs/plugin-sdk.md +212 -0
  99. package/package.json +11 -8
  100. package/src/cli/commands/audit.ts +318 -0
  101. package/src/cli/commands/circuit.ts +63 -0
  102. package/src/cli/commands/config.ts +176 -0
  103. package/src/cli/commands/init.ts +87 -145
  104. package/src/cli/commands/marketplace.ts +1 -1
  105. package/src/cli/commands/onboard.ts +33 -11
  106. package/src/cli/commands/release.ts +13 -6
  107. package/src/cli/index.ts +165 -11
  108. package/src/core/audit.ts +5 -2
  109. package/src/core/conductor.ts +11 -0
  110. package/src/core/config.ts +47 -2
  111. package/src/core/database.ts +32 -0
  112. package/src/core/encryption.ts +110 -0
  113. package/src/core/zero-config.ts +1 -5
  114. package/src/dashboard/server.ts +135 -16
  115. package/src/mcp/server.ts +40 -2
  116. package/src/plugins/builtin/aws.ts +162 -0
  117. package/src/plugins/builtin/database.ts +19 -1
  118. package/src/plugins/builtin/docker.ts +17 -1
  119. package/src/plugins/builtin/gcp.ts +145 -0
  120. package/src/plugins/builtin/index.ts +4 -0
  121. package/src/plugins/builtin/jira.ts +23 -19
  122. package/src/plugins/builtin/linear.ts +1 -1
  123. package/src/plugins/builtin/shell.ts +1 -1
  124. package/src/plugins/builtin/slack.ts +6 -1
  125. package/src/plugins/builtin/spotify.ts +1 -1
  126. package/src/plugins/builtin/vercel.ts +3 -1
  127. package/src/security/sso.ts +124 -0
  128. package/tests/audit.test.ts +185 -0
  129. package/tests/circuit-breaker.test.ts +125 -0
  130. package/tests/docker.test.ts +244 -39
  131. package/tests/errors.test.ts +122 -0
  132. package/tests/github.test.ts.skip +392 -0
  133. package/tests/jira.test.ts +310 -0
  134. package/tests/linear.test.ts +366 -0
  135. package/tests/mcp.test.ts.skip +243 -0
  136. package/tests/notion.test.ts +257 -0
  137. package/tests/retry.test.ts +104 -0
  138. package/tests/shell.test.ts +262 -30
  139. package/tests/slack.test.ts +250 -0
  140. package/tests/stripe.test.ts +272 -0
  141. package/tests/validation.test.ts +173 -0
  142. package/tests/vercel.test.ts +368 -0
  143. package/tests/zero-config.test.ts +566 -0
  144. package/C.png +0 -0
  145. package/tests/mcp.test.ts +0 -14
@@ -0,0 +1,566 @@
1
+ /**
2
+ * Tests for all zero-config plugins — no API keys required.
3
+ * Covers: calculator, colors, hash, text-tools, timezone.
4
+ */
5
+ import { describe, it, expect } from 'vitest';
6
+ import { CalculatorPlugin } from '../src/plugins/builtin/calculator.js';
7
+ import { ColorPlugin } from '../src/plugins/builtin/colors.js';
8
+ import { HashPlugin } from '../src/plugins/builtin/hash.js';
9
+ import { TextToolsPlugin } from '../src/plugins/builtin/text-tools.js';
10
+ import { TimezonePlugin } from '../src/plugins/builtin/timezone.js';
11
+
12
+ // ── Helpers ───────────────────────────────────────────────────────────────────
13
+
14
+ function getHandler<T = unknown>(
15
+ plugin: { getTools(): { name: string; handler: (i: unknown) => Promise<unknown> }[] },
16
+ toolName: string,
17
+ ): (input: T) => Promise<unknown> {
18
+ const tool = plugin.getTools().find((t) => t.name === toolName);
19
+ if (!tool) throw new Error(`Tool ${toolName} not found`);
20
+ return tool.handler as (input: T) => Promise<unknown>;
21
+ }
22
+
23
+ // ── Calculator ────────────────────────────────────────────────────────────────
24
+
25
+ describe('CalculatorPlugin', () => {
26
+ const plugin = new CalculatorPlugin();
27
+
28
+ it('is always configured', () => {
29
+ expect(plugin.isConfigured()).toBe(true);
30
+ });
31
+
32
+ it('exposes calc_math, calc_convert, calc_date tools', () => {
33
+ const names = plugin.getTools().map((t) => t.name);
34
+ expect(names).toContain('calc_math');
35
+ expect(names).toContain('calc_convert');
36
+ expect(names).toContain('calc_date');
37
+ });
38
+
39
+ describe('calc_math', () => {
40
+ const fn = () => getHandler<{ expression: string }>(plugin, 'calc_math');
41
+
42
+ it('evaluates addition', async () => {
43
+ const r = (await fn()({ expression: '2 + 2' })) as { result: unknown };
44
+ expect(r.result).toBe(4);
45
+ });
46
+
47
+ it('evaluates sqrt', async () => {
48
+ const r = (await fn()({ expression: 'sqrt(144)' })) as { result: unknown };
49
+ expect(r.result).toBe(12);
50
+ });
51
+
52
+ it('evaluates combined expression', async () => {
53
+ const r = (await fn()({ expression: 'sqrt(144) + 2^3' })) as { result: unknown };
54
+ expect(r.result).toBe(20);
55
+ });
56
+
57
+ it('evaluates PI', async () => {
58
+ const r = (await fn()({ expression: 'PI' })) as { result: number };
59
+ expect(r.result).toBeCloseTo(Math.PI, 5);
60
+ });
61
+
62
+ it('evaluates sin(0)', async () => {
63
+ const r = (await fn()({ expression: 'sin(0)' })) as { result: number };
64
+ expect(r.result).toBe(0);
65
+ });
66
+
67
+ it('evaluates floor / ceil / round', async () => {
68
+ expect(((await fn()({ expression: 'floor(3.9)' })) as { result: unknown }).result).toBe(3);
69
+ expect(((await fn()({ expression: 'ceil(3.1)' })) as { result: unknown }).result).toBe(4);
70
+ expect(((await fn()({ expression: 'round(3.5)' })) as { result: unknown }).result).toBe(4);
71
+ });
72
+
73
+ it('throws on invalid expression', async () => {
74
+ await expect(fn()({ expression: 'not_a_function_xyz()' })).rejects.toThrow();
75
+ });
76
+
77
+ it('respects order of operations with parentheses', async () => {
78
+ const r = (await fn()({ expression: '(2 + 3) * 4' })) as { result: unknown };
79
+ expect(r.result).toBe(20);
80
+ });
81
+ });
82
+
83
+ describe('calc_convert', () => {
84
+ const fn = () => getHandler<{ value: number; from: string; to: string }>(plugin, 'calc_convert');
85
+
86
+ it('converts km to mi', async () => {
87
+ const r = (await fn()({ value: 1, from: 'km', to: 'mi' })) as { result: number };
88
+ expect(r.result).toBeCloseTo(0.621371, 3);
89
+ });
90
+
91
+ it('converts mi to km', async () => {
92
+ const r = (await fn()({ value: 1, from: 'mi', to: 'km' })) as { result: number };
93
+ expect(r.result).toBeCloseTo(1.60934, 3);
94
+ });
95
+
96
+ it('converts kg to lb', async () => {
97
+ const r = (await fn()({ value: 1, from: 'kg', to: 'lb' })) as { result: number };
98
+ expect(r.result).toBeCloseTo(2.20462, 3);
99
+ });
100
+
101
+ it('converts 100°C to 212°F', async () => {
102
+ const r = (await fn()({ value: 100, from: 'c', to: 'f' })) as { result: number };
103
+ expect(r.result).toBe(212);
104
+ });
105
+
106
+ it('converts 32°F to 0°C', async () => {
107
+ const r = (await fn()({ value: 32, from: 'f', to: 'c' })) as { result: number };
108
+ expect(r.result).toBe(0);
109
+ });
110
+
111
+ it('converts 0°C to 273.15K', async () => {
112
+ const r = (await fn()({ value: 0, from: 'c', to: 'k' })) as { result: number };
113
+ expect(r.result).toBe(273.15);
114
+ });
115
+
116
+ it('converts 1 GB to 1024 MB', async () => {
117
+ const r = (await fn()({ value: 1, from: 'gb', to: 'mb' })) as { result: number };
118
+ expect(r.result).toBe(1024);
119
+ });
120
+
121
+ it('converts 1 L to ~0.264 gal', async () => {
122
+ const r = (await fn()({ value: 1, from: 'l', to: 'gal' })) as { result: number };
123
+ expect(r.result).toBeCloseTo(0.264172, 4);
124
+ });
125
+
126
+ it('returns same value when from === to', async () => {
127
+ const r = (await fn()({ value: 42, from: 'km', to: 'km' })) as { result: number };
128
+ expect(r.result).toBe(42);
129
+ });
130
+
131
+ it('throws on unsupported conversion', async () => {
132
+ await expect(fn()({ value: 1, from: 'parsecs', to: 'lightyears' })).rejects.toThrow();
133
+ });
134
+ });
135
+
136
+ describe('calc_date', () => {
137
+ const fn = () =>
138
+ getHandler<{ date: string; add_days?: number; end_date?: string }>(plugin, 'calc_date');
139
+
140
+ it('calculates days between two dates', async () => {
141
+ const r = (await fn()({ date: '2024-01-01', end_date: '2024-01-31' })) as { days: number };
142
+ expect(r.days).toBe(30);
143
+ });
144
+
145
+ it('adds days to a date', async () => {
146
+ const r = (await fn()({ date: '2024-01-01', add_days: 10 })) as { result: string };
147
+ expect(r.result).toBe('2024-01-11');
148
+ });
149
+
150
+ it('subtracts days with negative add_days', async () => {
151
+ const r = (await fn()({ date: '2024-01-11', add_days: -10 })) as { result: string };
152
+ expect(r.result).toBe('2024-01-01');
153
+ });
154
+
155
+ it('handles "today" as date', async () => {
156
+ const today = new Date().toISOString().split('T')[0];
157
+ const r = (await fn()({ date: 'today', add_days: 0 })) as { result: string };
158
+ expect(r.result).toBe(today);
159
+ });
160
+
161
+ it('throws on invalid date', async () => {
162
+ await expect(fn()({ date: 'not-a-date' })).rejects.toThrow();
163
+ });
164
+
165
+ it('returns day_of_week when adding days', async () => {
166
+ const r = (await fn()({ date: '2024-01-01', add_days: 0 })) as { day_of_week: string };
167
+ expect(r.day_of_week).toBeTruthy();
168
+ });
169
+ });
170
+ });
171
+
172
+ // ── Colors ────────────────────────────────────────────────────────────────────
173
+
174
+ describe('ColorPlugin', () => {
175
+ const plugin = new ColorPlugin();
176
+
177
+ it('is always configured', () => {
178
+ expect(plugin.isConfigured()).toBe(true);
179
+ });
180
+
181
+ it('exposes color_convert, color_contrast, color_palette', () => {
182
+ const names = plugin.getTools().map((t) => t.name);
183
+ expect(names).toContain('color_convert');
184
+ expect(names).toContain('color_contrast');
185
+ expect(names).toContain('color_palette');
186
+ });
187
+
188
+ describe('color_convert', () => {
189
+ const fn = () => getHandler<{ color: string }>(plugin, 'color_convert');
190
+
191
+ it('returns hex, rgb (string), hsl (string), and values', async () => {
192
+ const r = (await fn()({ color: '#ff0000' })) as { hex: string; rgb: string; hsl: string; values: Record<string, number> };
193
+ expect(r.hex).toBeDefined();
194
+ expect(r.rgb).toBeDefined();
195
+ expect(r.hsl).toBeDefined();
196
+ expect(r.values).toBeDefined();
197
+ });
198
+
199
+ it('converts red — rgb string format', async () => {
200
+ const r = (await fn()({ color: '#ff0000' })) as { rgb: string; values: { r: number; g: number; b: number } };
201
+ expect(r.rgb).toContain('255');
202
+ expect(r.values.r).toBe(255);
203
+ expect(r.values.g).toBe(0);
204
+ expect(r.values.b).toBe(0);
205
+ });
206
+
207
+ it('converts white', async () => {
208
+ const r = (await fn()({ color: '#ffffff' })) as { values: { r: number; g: number; b: number } };
209
+ expect(r.values.r).toBe(255);
210
+ expect(r.values.g).toBe(255);
211
+ expect(r.values.b).toBe(255);
212
+ });
213
+
214
+ it('converts black', async () => {
215
+ const r = (await fn()({ color: '#000000' })) as { values: { r: number; g: number; b: number } };
216
+ expect(r.values.r).toBe(0);
217
+ expect(r.values.g).toBe(0);
218
+ expect(r.values.b).toBe(0);
219
+ });
220
+
221
+ it('handles 3-digit hex', async () => {
222
+ const r = (await fn()({ color: '#f00' })) as { values: { r: number; g: number; b: number } };
223
+ expect(r.values.r).toBe(255);
224
+ expect(r.values.g).toBe(0);
225
+ expect(r.values.b).toBe(0);
226
+ });
227
+ });
228
+
229
+ describe('color_palette', () => {
230
+ // color_palette uses `base` not `base_color`
231
+ const fn = () => getHandler<{ base: string; type?: string }>(plugin, 'color_palette');
232
+
233
+ it('generates a palette from base color', async () => {
234
+ const r = (await fn()({ base: '#3b82f6' })) as Record<string, unknown>;
235
+ // response includes colors array
236
+ expect(r).toBeDefined();
237
+ });
238
+
239
+ it('generates complementary palette', async () => {
240
+ const r = (await fn()({ base: '#ff0000', type: 'complementary' })) as Record<string, unknown>;
241
+ expect(r).toBeDefined();
242
+ });
243
+
244
+ it('generates triadic palette', async () => {
245
+ const r = (await fn()({ base: '#ff0000', type: 'triadic' })) as Record<string, unknown>;
246
+ expect(r).toBeDefined();
247
+ });
248
+ });
249
+
250
+ describe('color_contrast', () => {
251
+ const fn = () =>
252
+ getHandler<{ foreground: string; background: string }>(plugin, 'color_contrast');
253
+
254
+ it('returns contrast ratio', async () => {
255
+ const r = (await fn()({ foreground: '#000000', background: '#ffffff' })) as { ratio: number };
256
+ expect(r.ratio).toBeGreaterThan(20); // ~21:1
257
+ });
258
+
259
+ it('passes AA for black on white (PASS string)', async () => {
260
+ const r = (await fn()({ foreground: '#000000', background: '#ffffff' })) as { aa_normal: string };
261
+ expect(r.aa_normal).toBe('PASS');
262
+ });
263
+
264
+ it('fails AA for same color (FAIL string)', async () => {
265
+ const r = (await fn()({ foreground: '#ffffff', background: '#ffffff' })) as { aa_normal: string };
266
+ expect(r.aa_normal).toBe('FAIL');
267
+ });
268
+ });
269
+ });
270
+
271
+ // ── Hash ──────────────────────────────────────────────────────────────────────
272
+
273
+ describe('HashPlugin', () => {
274
+ const plugin = new HashPlugin();
275
+
276
+ it('is always configured', () => {
277
+ expect(plugin.isConfigured()).toBe(true);
278
+ });
279
+
280
+ it('exposes hash_text, base64_encode, base64_decode, generate_uuid, generate_password', () => {
281
+ const names = plugin.getTools().map((t) => t.name);
282
+ expect(names).toContain('hash_text');
283
+ expect(names).toContain('base64_encode');
284
+ expect(names).toContain('base64_decode');
285
+ expect(names).toContain('generate_uuid');
286
+ expect(names).toContain('generate_password');
287
+ });
288
+
289
+ describe('hash_text', () => {
290
+ const fn = () => getHandler<{ text: string; algorithm?: string }>(plugin, 'hash_text');
291
+
292
+ it('hashes with sha256 by default', async () => {
293
+ const r = (await fn()({ text: 'hello' })) as { algorithm: string; hash: string };
294
+ expect(r.algorithm).toBe('sha256');
295
+ expect(r.hash).toBe('2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824');
296
+ });
297
+
298
+ it('hashes with md5', async () => {
299
+ const r = (await fn()({ text: 'hello', algorithm: 'md5' })) as { hash: string };
300
+ expect(r.hash).toBe('5d41402abc4b2a76b9719d911017c592');
301
+ });
302
+
303
+ it('hashes with sha512 (128 hex chars)', async () => {
304
+ const r = (await fn()({ text: 'hello', algorithm: 'sha512' })) as { hash: string };
305
+ expect(r.hash).toHaveLength(128);
306
+ });
307
+
308
+ it('produces consistent hashes', async () => {
309
+ const a = (await fn()({ text: 'test' })) as { hash: string };
310
+ const b = (await fn()({ text: 'test' })) as { hash: string };
311
+ expect(a.hash).toBe(b.hash);
312
+ });
313
+
314
+ it('different input → different hash', async () => {
315
+ const a = (await fn()({ text: 'hello' })) as { hash: string };
316
+ const b = (await fn()({ text: 'world' })) as { hash: string };
317
+ expect(a.hash).not.toBe(b.hash);
318
+ });
319
+ });
320
+
321
+ describe('base64_encode / base64_decode', () => {
322
+ const enc = () => getHandler<{ text: string }>(plugin, 'base64_encode');
323
+ const dec = () => getHandler<{ text: string }>(plugin, 'base64_decode');
324
+
325
+ it('encodes to base64', async () => {
326
+ const r = (await enc()({ text: 'hello world' })) as { encoded: string };
327
+ expect(r.encoded).toBe('aGVsbG8gd29ybGQ=');
328
+ });
329
+
330
+ it('decodes from base64', async () => {
331
+ const r = (await dec()({ text: 'aGVsbG8gd29ybGQ=' })) as { decoded: string };
332
+ expect(r.decoded).toBe('hello world');
333
+ });
334
+
335
+ it('round-trips encode/decode', async () => {
336
+ const original = 'The quick brown fox';
337
+ const { encoded } = (await enc()({ text: original })) as { encoded: string };
338
+ const { decoded } = (await dec()({ text: encoded })) as { decoded: string };
339
+ expect(decoded).toBe(original);
340
+ });
341
+ });
342
+
343
+ describe('generate_uuid', () => {
344
+ // Returns { uuids: string[] }
345
+ const fn = () => getHandler<{ count?: number }>(plugin, 'generate_uuid');
346
+
347
+ it('generates a valid UUID v4', async () => {
348
+ const r = (await fn()({})) as { uuids: string[] };
349
+ expect(r.uuids).toHaveLength(1);
350
+ expect(r.uuids[0]).toMatch(
351
+ /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
352
+ );
353
+ });
354
+
355
+ it('generates N unique UUIDs', async () => {
356
+ const r = (await fn()({ count: 5 })) as { uuids: string[] };
357
+ expect(r.uuids).toHaveLength(5);
358
+ expect(new Set(r.uuids).size).toBe(5);
359
+ });
360
+
361
+ it('caps at 50 UUIDs', async () => {
362
+ const r = (await fn()({ count: 100 })) as { uuids: string[] };
363
+ expect(r.uuids.length).toBeLessThanOrEqual(50);
364
+ });
365
+ });
366
+
367
+ describe('generate_password', () => {
368
+ const fn = () => getHandler<{ length?: number; symbols?: boolean }>(plugin, 'generate_password');
369
+
370
+ it('generates a password', async () => {
371
+ const r = (await fn()({})) as { password: string };
372
+ expect(typeof r.password).toBe('string');
373
+ expect(r.password.length).toBeGreaterThan(0);
374
+ });
375
+
376
+ it('respects length parameter', async () => {
377
+ const r = (await fn()({ length: 24 })) as { password: string };
378
+ expect(r.password.length).toBe(24);
379
+ });
380
+ });
381
+ });
382
+
383
+ // ── TextTools ─────────────────────────────────────────────────────────────────
384
+
385
+ describe('TextToolsPlugin', () => {
386
+ const plugin = new TextToolsPlugin();
387
+
388
+ it('is always configured', () => {
389
+ expect(plugin.isConfigured()).toBe(true);
390
+ });
391
+
392
+ it('exposes json_format, text_stats, regex_test, text_transform', () => {
393
+ const names = plugin.getTools().map((t) => t.name);
394
+ expect(names).toContain('json_format');
395
+ expect(names).toContain('text_stats');
396
+ expect(names).toContain('regex_test');
397
+ expect(names).toContain('text_transform');
398
+ });
399
+
400
+ describe('json_format', () => {
401
+ const fn = () => getHandler<{ json: string; minify?: boolean }>(plugin, 'json_format');
402
+
403
+ it('pretty-prints valid JSON', async () => {
404
+ const r = (await fn()({ json: '{"a":1}' })) as { valid: boolean; formatted: string };
405
+ expect(r.valid).toBe(true);
406
+ expect(r.formatted).toContain('\n');
407
+ });
408
+
409
+ it('minifies JSON', async () => {
410
+ const r = (await fn()({ json: '{"a": 1, "b": 2}', minify: true })) as { formatted: string };
411
+ expect(r.formatted).toBe('{"a":1,"b":2}');
412
+ });
413
+
414
+ it('reports invalid JSON', async () => {
415
+ const r = (await fn()({ json: '{invalid}' })) as { valid: boolean };
416
+ expect(r.valid).toBe(false);
417
+ });
418
+ });
419
+
420
+ describe('text_stats', () => {
421
+ const fn = () => getHandler<{ text: string }>(plugin, 'text_stats');
422
+
423
+ it('counts words correctly', async () => {
424
+ const r = (await fn()({ text: 'Hello world foo bar' })) as { words: number };
425
+ expect(r.words).toBe(4);
426
+ });
427
+
428
+ it('counts characters', async () => {
429
+ const r = (await fn()({ text: 'hello' })) as { characters: number };
430
+ expect(r.characters).toBe(5);
431
+ });
432
+
433
+ it('returns reading time', async () => {
434
+ const r = (await fn()({ text: 'word '.repeat(200) })) as { reading_time: string };
435
+ expect(r.reading_time).toContain('min');
436
+ });
437
+
438
+ it('handles empty string', async () => {
439
+ const r = (await fn()({ text: '' })) as { words: number };
440
+ expect(r.words).toBe(0);
441
+ });
442
+ });
443
+
444
+ describe('regex_test', () => {
445
+ // Returns { matches, match_count (or count), ... }
446
+ const fn = () => getHandler<{ pattern: string; text: string; flags?: string }>(plugin, 'regex_test');
447
+
448
+ it('finds matches and returns count', async () => {
449
+ const r = (await fn()({ pattern: '\\d+', text: 'abc 123 def 456' })) as Record<string, unknown>;
450
+ // could be match_count or count depending on implementation
451
+ const count = (r.match_count ?? r.count ?? (r.matches as unknown[])?.length) as number;
452
+ expect(count).toBe(2);
453
+ });
454
+
455
+ it('returns zero matches when no match', async () => {
456
+ const r = (await fn()({ pattern: '\\d+', text: 'no numbers here' })) as Record<string, unknown>;
457
+ const count = (r.match_count ?? r.count ?? (r.matches as unknown[])?.length) as number;
458
+ expect(count).toBe(0);
459
+ });
460
+
461
+ it('handles case-insensitive flag', async () => {
462
+ const r = (await fn()({ pattern: 'hello', text: 'Hello HELLO hello', flags: 'gi' })) as Record<string, unknown>;
463
+ const count = (r.match_count ?? r.count ?? (r.matches as unknown[])?.length) as number;
464
+ expect(count).toBe(3);
465
+ });
466
+ });
467
+
468
+ describe('text_transform', () => {
469
+ // Tool is named text_transform, parameter is `transform` not `operation`
470
+ const fn = () => getHandler<{ text: string; transform: string }>(plugin, 'text_transform');
471
+
472
+ it('uppercases text', async () => {
473
+ const r = (await fn()({ text: 'hello', transform: 'uppercase' })) as { result: string };
474
+ expect(r.result).toBe('HELLO');
475
+ });
476
+
477
+ it('lowercases text', async () => {
478
+ const r = (await fn()({ text: 'HELLO', transform: 'lowercase' })) as { result: string };
479
+ expect(r.result).toBe('hello');
480
+ });
481
+
482
+ it('reverses text', async () => {
483
+ const r = (await fn()({ text: 'hello', transform: 'reverse' })) as { result: string };
484
+ expect(r.result).toBe('olleh');
485
+ });
486
+
487
+ it('converts to camelCase', async () => {
488
+ const r = (await fn()({ text: 'hello world foo', transform: 'camel' })) as { result: string };
489
+ expect(r.result).toContain('World');
490
+ });
491
+
492
+ it('converts to title case', async () => {
493
+ const r = (await fn()({ text: 'hello world', transform: 'title' })) as { result: string };
494
+ expect(r.result.charAt(0)).toBe('H');
495
+ });
496
+
497
+ it('converts to snake_case', async () => {
498
+ const r = (await fn()({ text: 'hello world', transform: 'snake' })) as { result: string };
499
+ expect(r.result).toContain('_');
500
+ });
501
+
502
+ it('converts to slug', async () => {
503
+ const r = (await fn()({ text: 'Hello World', transform: 'slug' })) as { result: string };
504
+ expect(r.result).toContain('-');
505
+ expect(r.result).not.toContain(' ');
506
+ });
507
+ });
508
+ });
509
+
510
+ // ── Timezone ─────────────────────────────────────────────────────────────────
511
+
512
+ describe('TimezonePlugin', () => {
513
+ const plugin = new TimezonePlugin();
514
+
515
+ it('is always configured', () => {
516
+ expect(plugin.isConfigured()).toBe(true);
517
+ });
518
+
519
+ it('exposes time_now and time_convert', () => {
520
+ const names = plugin.getTools().map((t) => t.name);
521
+ expect(names).toContain('time_now');
522
+ expect(names).toContain('time_convert');
523
+ });
524
+
525
+ describe('time_now', () => {
526
+ // time_now takes { cities: string[] } — required array
527
+ const fn = () => getHandler<{ cities: string[] }>(plugin, 'time_now');
528
+
529
+ it('returns current time in UTC', async () => {
530
+ const r = (await fn()({ cities: ['UTC'] })) as Array<{ city: string; timezone: string; time: string }>;
531
+ expect(Array.isArray(r)).toBe(true);
532
+ expect(r[0].city).toBe('UTC');
533
+ expect(r[0].time).toBeTruthy();
534
+ });
535
+
536
+ it('returns current time in Tokyo', async () => {
537
+ const r = (await fn()({ cities: ['Tokyo'] })) as Array<{ city: string }>;
538
+ expect(r[0].city).toBe('Tokyo');
539
+ });
540
+
541
+ it('returns current time in New York', async () => {
542
+ const r = (await fn()({ cities: ['New York'] })) as Array<{ city: string }>;
543
+ expect(r[0].city).toBe('New York');
544
+ });
545
+
546
+ it('returns multiple cities at once', async () => {
547
+ const r = (await fn()({ cities: ['Tokyo', 'London', 'New York'] })) as Array<{ city: string }>;
548
+ expect(r).toHaveLength(3);
549
+ expect(r.map((c) => c.city)).toEqual(['Tokyo', 'London', 'New York']);
550
+ });
551
+ });
552
+
553
+ describe('time_convert', () => {
554
+ const fn = () => getHandler<{ time: string; from: string; to: string }>(plugin, 'time_convert');
555
+
556
+ it('converts time between UTC and America/New_York', async () => {
557
+ const r = await fn()({ time: '12:00', from: 'UTC', to: 'New York' });
558
+ expect(r).toBeDefined();
559
+ });
560
+
561
+ it('converts from Tokyo to London', async () => {
562
+ const r = await fn()({ time: '09:00', from: 'Tokyo', to: 'London' });
563
+ expect(r).toBeDefined();
564
+ });
565
+ });
566
+ });
package/C.png DELETED
Binary file
package/tests/mcp.test.ts DELETED
@@ -1,14 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
-
3
- describe('MCP Server', () => {
4
- it('exports startMCPServer function', async () => {
5
- const { startMCPServer } = await import('../src/mcp/server.js');
6
- expect(typeof startMCPServer).toBe('function');
7
- });
8
-
9
- it('builds tool registry with builtin tools', async () => {
10
- // This tests that the MCP server can start and register tools
11
- const { startMCPServer } = await import('../src/mcp/server.js');
12
- expect(startMCPServer).toBeDefined();
13
- });
14
- });