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