/ src / lib / server / plugins-advanced.test.ts
plugins-advanced.test.ts
  1  import { describe, it } from 'node:test'
  2  import assert from 'node:assert/strict'
  3  import {
  4    canonicalizeExtensionId,
  5    expandExtensionIds,
  6    getExtensionAliases,
  7    normalizeExtensionId,
  8    extensionIdMatches,
  9  } from './tool-aliases'
 10  
 11  // ---------------------------------------------------------------------------
 12  // normalizeExtensionId
 13  // ---------------------------------------------------------------------------
 14  describe('normalizeExtensionId', () => {
 15    it('converts uppercase to lowercase', () => {
 16      assert.equal(normalizeExtensionId('WEB_SEARCH'), 'web_search')
 17    })
 18  
 19    it('trims leading and trailing whitespace', () => {
 20      assert.equal(normalizeExtensionId('  shell  '), 'shell')
 21    })
 22  
 23    it('handles combined upper + whitespace', () => {
 24      assert.equal(normalizeExtensionId('  WEB_SEARCH  '), 'web_search')
 25    })
 26  
 27    it('returns empty string for empty input', () => {
 28      assert.equal(normalizeExtensionId(''), '')
 29    })
 30  
 31    it('returns already normalized value unchanged', () => {
 32      assert.equal(normalizeExtensionId('files'), 'files')
 33    })
 34  
 35    it('returns empty string for non-string input (number)', () => {
 36      assert.equal(normalizeExtensionId(42), '')
 37    })
 38  
 39    it('returns empty string for null', () => {
 40      assert.equal(normalizeExtensionId(null), '')
 41    })
 42  
 43    it('returns empty string for undefined', () => {
 44      assert.equal(normalizeExtensionId(undefined), '')
 45    })
 46  })
 47  
 48  // ---------------------------------------------------------------------------
 49  // canonicalizeExtensionId
 50  // ---------------------------------------------------------------------------
 51  describe('canonicalizeExtensionId', () => {
 52    it('resolves web_search → web', () => {
 53      assert.equal(canonicalizeExtensionId('web_search'), 'web')
 54    })
 55  
 56    it('resolves web_fetch → web', () => {
 57      assert.equal(canonicalizeExtensionId('web_fetch'), 'web')
 58    })
 59  
 60    it('keeps web (already canonical)', () => {
 61      assert.equal(canonicalizeExtensionId('web'), 'web')
 62    })
 63  
 64    it('resolves execute_command → shell', () => {
 65      assert.equal(canonicalizeExtensionId('execute_command'), 'shell')
 66    })
 67  
 68    it('resolves memory_tool → memory', () => {
 69      assert.equal(canonicalizeExtensionId('memory_tool'), 'memory')
 70    })
 71  
 72    it('resolves narrow memory tools → memory', () => {
 73      assert.equal(canonicalizeExtensionId('memory_search'), 'memory')
 74      assert.equal(canonicalizeExtensionId('memory_get'), 'memory')
 75      assert.equal(canonicalizeExtensionId('memory_store'), 'memory')
 76      assert.equal(canonicalizeExtensionId('memory_update'), 'memory')
 77    })
 78  
 79    it('keeps files (already canonical)', () => {
 80      assert.equal(canonicalizeExtensionId('files'), 'files')
 81    })
 82  
 83    it('returns unknown extension as-is', () => {
 84      assert.equal(canonicalizeExtensionId('totally_unknown'), 'totally_unknown')
 85    })
 86  
 87    it('resolves delegate_to_claude_code → delegate', () => {
 88      assert.equal(canonicalizeExtensionId('delegate_to_claude_code'), 'delegate')
 89    })
 90  
 91    it('resolves claude_code → delegate', () => {
 92      assert.equal(canonicalizeExtensionId('claude_code'), 'delegate')
 93    })
 94  
 95    it('resolves process_tool → shell', () => {
 96      assert.equal(canonicalizeExtensionId('process_tool'), 'shell')
 97    })
 98  
 99    it('resolves openclaw_browser → browser', () => {
100      assert.equal(canonicalizeExtensionId('openclaw_browser'), 'browser')
101    })
102  
103    it('returns raw string (preserving case) for empty normalized result', () => {
104      // non-string input → normalizeExtensionId returns ''
105      assert.equal(canonicalizeExtensionId(123), '')
106    })
107  })
108  
109  // ---------------------------------------------------------------------------
110  // expandExtensionIds
111  // ---------------------------------------------------------------------------
112  describe('expandExtensionIds', () => {
113    it('shell implies process', () => {
114      const result = expandExtensionIds(['shell'])
115      assert.ok(result.includes('shell'))
116      assert.ok(result.includes('process'))
117    })
118  
119    it('manage_platform expands to sub-extensions', () => {
120      const result = expandExtensionIds(['manage_platform'])
121      const expected = [
122        'manage_platform',
123        'manage_agents',
124        'manage_projects',
125        'manage_tasks',
126        'manage_schedules',
127        'manage_skills',
128        'manage_webhooks',
129        'manage_connectors',
130        'manage_sessions',
131        'manage_secrets',
132      ]
133      for (const e of expected) {
134        assert.ok(result.includes(e), `expected ${e} in expansion`)
135      }
136    })
137  
138    it('web expands to include web_search and web_fetch', () => {
139      const result = expandExtensionIds(['web'])
140      assert.ok(result.includes('web'))
141      assert.ok(result.includes('web_search'))
142      assert.ok(result.includes('web_fetch'))
143    })
144  
145    it('removes duplicates after expansion', () => {
146      const result = expandExtensionIds(['web', 'web_search', 'web_fetch'])
147      const unique = new Set(result)
148      assert.equal(result.length, unique.size)
149    })
150  
151    it('returns empty array for empty input', () => {
152      assert.deepEqual(expandExtensionIds([]), [])
153    })
154  
155    it('keeps unknown extension as-is', () => {
156      const result = expandExtensionIds(['my_custom_extension'])
157      assert.ok(result.includes('my_custom_extension'))
158    })
159  
160    it('deduplicates overlapping expansions from multiple inputs', () => {
161      const result = expandExtensionIds(['web', 'web_search'])
162      const counts = result.reduce<Record<string, number>>((acc, id) => {
163        acc[id] = (acc[id] || 0) + 1
164        return acc
165      }, {})
166      for (const [id, count] of Object.entries(counts)) {
167        assert.equal(count, 1, `${id} appears ${count} times`)
168      }
169    })
170  
171    it('returns empty array for null', () => {
172      assert.deepEqual(expandExtensionIds(null), [])
173    })
174  
175    it('returns empty array for undefined', () => {
176      assert.deepEqual(expandExtensionIds(undefined), [])
177    })
178  
179    it('shell also expands aliases (execute_command, process_tool)', () => {
180      const result = expandExtensionIds(['shell'])
181      assert.ok(result.includes('execute_command'))
182      assert.ok(result.includes('process_tool'))
183    })
184  
185    it('manage_platform + shell has no duplicates', () => {
186      const result = expandExtensionIds(['manage_platform', 'shell'])
187      const unique = new Set(result)
188      assert.equal(result.length, unique.size)
189    })
190  
191    it('handles same extension requested multiple times', () => {
192      const result = expandExtensionIds(['web', 'web', 'web'])
193      const webCount = result.filter((id) => id === 'web').length
194      assert.equal(webCount, 1)
195    })
196  })
197  
198  // ---------------------------------------------------------------------------
199  // getExtensionAliases
200  // ---------------------------------------------------------------------------
201  describe('getExtensionAliases', () => {
202    it('web returns [web, web_search, web_fetch]', () => {
203      const result = getExtensionAliases('web')
204      assert.ok(result.includes('web'))
205      assert.ok(result.includes('web_search'))
206      assert.ok(result.includes('web_fetch'))
207      assert.equal(result.length, 5) // web, web_search, web_fetch, http_request, http
208    })
209  
210    it('web_search returns the same group as web', () => {
211      const fromWeb = getExtensionAliases('web').sort()
212      const fromAlias = getExtensionAliases('web_search').sort()
213      assert.deepEqual(fromWeb, fromAlias)
214    })
215  
216    it('unknown extension returns array with just the input', () => {
217      assert.deepEqual(getExtensionAliases('unknown_thing'), ['unknown_thing'])
218    })
219  
220    it('shell includes execute_command and process_tool', () => {
221      const result = getExtensionAliases('shell')
222      assert.ok(result.includes('shell'))
223      assert.ok(result.includes('execute_command'))
224      assert.ok(result.includes('process_tool'))
225    })
226  
227    it('returns empty array for empty string', () => {
228      assert.deepEqual(getExtensionAliases(''), [])
229    })
230  
231    it('returns empty array for null', () => {
232      assert.deepEqual(getExtensionAliases(null), [])
233    })
234  
235    it('delegate group includes all delegate variants', () => {
236      const result = getExtensionAliases('delegate')
237      assert.ok(result.includes('claude_code'))
238      assert.ok(result.includes('delegate_to_claude_code'))
239      assert.ok(result.includes('codex_cli'))
240      assert.ok(result.includes('delegate_to_codex_cli'))
241    })
242  })
243  
244  // ---------------------------------------------------------------------------
245  // extensionIdMatches
246  // ---------------------------------------------------------------------------
247  describe('extensionIdMatches', () => {
248    it('web enabled, web_search matches (alias)', () => {
249      assert.equal(extensionIdMatches(['web'], 'web_search'), true)
250    })
251  
252    it('web_search enabled, web matches (reverse alias)', () => {
253      assert.equal(extensionIdMatches(['web_search'], 'web'), true)
254    })
255  
256    it('files enabled, shell does not match (different families)', () => {
257      assert.equal(extensionIdMatches(['files'], 'shell'), false)
258    })
259  
260    it('manage_platform enabled, manage_tasks matches (implication)', () => {
261      assert.equal(extensionIdMatches(['manage_platform'], 'manage_tasks'), true)
262    })
263  
264    it('empty enabled list, nothing matches', () => {
265      assert.equal(extensionIdMatches([], 'web'), false)
266    })
267  
268    it('case insensitive match', () => {
269      assert.equal(extensionIdMatches(['WEB'], 'web_search'), true)
270    })
271  
272    it('shell enabled, process matches (implication)', () => {
273      assert.equal(extensionIdMatches(['shell'], 'process'), true)
274    })
275  
276    it('manage_platform enabled, manage_secrets matches', () => {
277      assert.equal(extensionIdMatches(['manage_platform'], 'manage_secrets'), true)
278    })
279  
280    it('null enabled list returns false', () => {
281      assert.equal(extensionIdMatches(null, 'web'), false)
282    })
283  
284    it('undefined enabled list returns false', () => {
285      assert.equal(extensionIdMatches(undefined, 'web'), false)
286    })
287  })
288  
289  // ---------------------------------------------------------------------------
290  // Complex expansion scenarios
291  // ---------------------------------------------------------------------------
292  describe('complex expansion scenarios', () => {
293    it('shell + web + memory fully expands', () => {
294      const result = expandExtensionIds(['shell', 'web', 'memory'])
295      // shell family
296      assert.ok(result.includes('shell'))
297      assert.ok(result.includes('execute_command'))
298      assert.ok(result.includes('process_tool'))
299      assert.ok(result.includes('process'))
300      // web family
301      assert.ok(result.includes('web'))
302      assert.ok(result.includes('web_search'))
303      assert.ok(result.includes('web_fetch'))
304      // memory family
305      assert.ok(result.includes('memory'))
306      assert.ok(result.includes('memory_tool'))
307    })
308  
309    it('large extension list (50+ items) all expanded correctly', () => {
310      const ids = Array.from({ length: 50 }, (_, i) => `custom_extension_${i}`)
311      ids.push('shell', 'web')
312      const result = expandExtensionIds(ids)
313      // All custom ones present
314      for (let i = 0; i < 50; i++) {
315        assert.ok(result.includes(`custom_extension_${i}`))
316      }
317      // Shell expansion present
318      assert.ok(result.includes('process'))
319      // Web expansion present
320      assert.ok(result.includes('web_fetch'))
321    })
322  
323    it('alias chains do not cause infinite loops', () => {
324      // delegate has many aliases; expansion should terminate
325      const result = expandExtensionIds(['delegate'])
326      assert.ok(result.includes('delegate'))
327      assert.ok(result.includes('claude_code'))
328      assert.ok(result.includes('delegate_to_claude_code'))
329      // Just confirm it returned without hanging
330      assert.ok(result.length > 0)
331    })
332  
333    it('connector aliases expand correctly', () => {
334      const result = expandExtensionIds(['manage_connectors'])
335      assert.ok(result.includes('manage_connectors'))
336      assert.ok(result.includes('connectors'))
337      assert.ok(result.includes('connector_message_tool'))
338    })
339  
340    it('sandbox aliases expand', () => {
341      const result = expandExtensionIds(['sandbox'])
342      assert.ok(result.includes('sandbox'))
343      assert.ok(result.includes('execute'))
344    })
345  
346    it('files expands to include read_file, write_file, etc.', () => {
347      const result = expandExtensionIds(['files'])
348      assert.ok(result.includes('read_file'))
349      assert.ok(result.includes('write_file'))
350      assert.ok(result.includes('list_files'))
351      assert.ok(result.includes('copy_file'))
352      assert.ok(result.includes('move_file'))
353      assert.ok(result.includes('delete_file'))
354      assert.ok(result.includes('send_file'))
355    })
356  })