#
tokens: 25168/50000 1/12 files (page 2/3)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 2 of 3. Use http://codebase.md/furey/mongodb-lens?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .dockerhub
│   └── overview.md
├── .dockerignore
├── .github
│   ├── FUNDING.yml
│   └── workflows
│       ├── publish-docker.yml
│       └── publish-npm.yml
├── .gitignore
├── config-create.js
├── Dockerfile
├── LICENSE
├── mongodb-lens.js
├── mongodb-lens.test.js
├── package-lock.json
├── package.json
└── README.md
```

# Files

--------------------------------------------------------------------------------
/mongodb-lens.test.js:
--------------------------------------------------------------------------------

```javascript
   1 | #!/usr/bin/env node
   2 | 
   3 | import { spawn } from 'child_process'
   4 | import { dirname, join } from 'path'
   5 | import { fileURLToPath } from 'url'
   6 | import { existsSync } from 'fs'
   7 | import mongodb from 'mongodb'
   8 | 
   9 | const { MongoClient, ObjectId } = mongodb
  10 | 
  11 | const __filename = fileURLToPath(import.meta.url)
  12 | const __dirname = dirname(__filename)
  13 | 
  14 | const runTests = async () => {
  15 |   parseCommandLineArgs()
  16 | 
  17 |   if (process.argv.includes('--list')) return listAllTests()
  18 | 
  19 |   try {
  20 |     logHeader('MongoDB Lens Test Suite', 'margin:bottom')
  21 | 
  22 |     await initialize()
  23 | 
  24 |     logHeader('Testing Tools', 'margin:top,bottom')
  25 | 
  26 |     for (const group of TEST_GROUPS) {
  27 |       if (group.name !== 'Resources' && group.name !== 'Prompts') {
  28 |         await runTestGroup(group.name, group.tests)
  29 |       }
  30 |     }
  31 | 
  32 |     logHeader('Testing Resources', 'margin:top,bottom')
  33 | 
  34 |     const resourcesGroup = TEST_GROUPS.find(g => g.name === 'Resources')
  35 |     if (resourcesGroup) await runTestGroup(resourcesGroup.name, resourcesGroup.tests)
  36 | 
  37 |     logHeader('Testing Prompts', 'margin:top,bottom')
  38 | 
  39 |     const promptsGroup = TEST_GROUPS.find(g => g.name === 'Prompts')
  40 |     if (promptsGroup) await runTestGroup(promptsGroup.name, promptsGroup.tests)
  41 |   } finally {
  42 |     await cleanup()
  43 |     displayTestSummary()
  44 |   }
  45 | }
  46 | 
  47 | const runTestGroup = async (groupName, tests) => {
  48 |   let anyTestsInGroup = false
  49 | 
  50 |   for (const test of tests) {
  51 |     if (shouldRunTest(test.name, groupName)) {
  52 |       anyTestsInGroup = true
  53 |       break
  54 |     }
  55 |   }
  56 | 
  57 |   if (!anyTestsInGroup) return console.log(`${COLORS.yellow}Skipping group: ${groupName} - No matching tests${COLORS.reset}`)
  58 | 
  59 |   console.log(`${COLORS.blue}Running tests in group: ${groupName}${COLORS.reset}`)
  60 | 
  61 |   for (const test of tests) {
  62 |     if (shouldRunTest(test.name, groupName)) {
  63 |       await runTest(test.name, test.fn)
  64 |     } else {
  65 |       skipTest(test.name, 'Not selected for execution')
  66 |     }
  67 |   }
  68 | }
  69 | 
  70 | const shouldRunTest = (testName, groupName) => {
  71 |   if (!testFilters.length && !groupFilters.length && !patternFilters.length) return true
  72 | 
  73 |   if (testFilters.includes(testName)) return true
  74 | 
  75 |   if (groupFilters.includes(groupName)) return true
  76 | 
  77 |   for (const pattern of patternFilters) {
  78 |     const regexPattern = pattern.replace(/\*/g, '.*')
  79 |     const regex = new RegExp(regexPattern, 'i')
  80 |     if (regex.test(testName)) return true
  81 |   }
  82 | 
  83 |   return false
  84 | }
  85 | 
  86 | const listAllTests = () => {
  87 |   logHeader('Available Test Groups & Tests', 'margin:bottom')
  88 | 
  89 |   TEST_GROUPS.forEach(group => {
  90 |     console.log(`${COLORS.yellow}${group.name}:${COLORS.reset}`)
  91 |     group.tests.forEach(test => {
  92 |       console.log(`- ${test.name}`)
  93 |     })
  94 |     console.log('')
  95 |   })
  96 | 
  97 |   console.log(`${COLORS.yellow}Usage:${COLORS.reset}`)
  98 |   console.log('  --test=<test-name>     Run specific test(s), comma separated')
  99 |   console.log('  --group=<group-name>   Run all tests in specific group(s), comma separated')
 100 |   console.log('  --pattern=<pattern>    Run tests matching pattern (glob style, e.g. *collection*)')
 101 |   console.log('  --list                 List all available tests without running them')
 102 | 
 103 |   process.exit(0)
 104 | }
 105 | 
 106 | const parseCommandLineArgs = () => {
 107 |   for (let i = 2; i < process.argv.length; i++) {
 108 |     const arg = process.argv[i]
 109 | 
 110 |     if (arg.startsWith('--test=')) {
 111 |       const tests = arg.replace('--test=', '').split(',')
 112 |       testFilters.push(...tests.map(t => t.trim()))
 113 |     } else if (arg.startsWith('--group=')) {
 114 |       const groups = arg.replace('--group=', '').split(',')
 115 |       groupFilters.push(...groups.map(g => g.trim()))
 116 |     } else if (arg.startsWith('--pattern=')) {
 117 |       const patterns = arg.replace('--pattern=', '').split(',')
 118 |       patternFilters.push(...patterns.map(p => p.trim()))
 119 |     }
 120 |   }
 121 | 
 122 |   if (testFilters.length || groupFilters.length || patternFilters.length) {
 123 |     console.log(`${COLORS.blue}Running with filters:${COLORS.reset}`)
 124 |     if (testFilters.length) console.log(`${COLORS.blue}Tests: ${testFilters.join(', ')}${COLORS.reset}`)
 125 |     if (groupFilters.length) console.log(`${COLORS.blue}Groups: ${groupFilters.join(', ')}${COLORS.reset}`)
 126 |     if (patternFilters.length) console.log(`${COLORS.blue}Patterns: ${patternFilters.join(', ')}${COLORS.reset}`)
 127 |   }
 128 | }
 129 | 
 130 | const initialize = async () => {
 131 |   mongoUri = await setupMongoUri()
 132 |   await connectToMongo()
 133 |   await setupTestEnvironment()
 134 | }
 135 | 
 136 | const setupMongoUri = async () => {
 137 |   const uri = process.env.CONFIG_MONGO_URI
 138 | 
 139 |   if (uri === 'mongodb-memory-server') return await startInMemoryMongo()
 140 | 
 141 |   if (!uri) {
 142 |     console.error(`${COLORS.red}No MongoDB URI provided. Please set CONFIG_MONGO_URI environment variable.${COLORS.reset}`)
 143 |     console.error(`${COLORS.yellow}Example: CONFIG_MONGO_URI=mongodb://localhost:27017 node mongodb-lens.test.js${COLORS.reset}`)
 144 |     console.error(`${COLORS.yellow}Example: CONFIG_MONGO_URI=mongodb-memory-server node mongodb-lens.test.js${COLORS.reset}`)
 145 |     process.exit(1)
 146 |   }
 147 | 
 148 |   return uri
 149 | }
 150 | 
 151 | const startInMemoryMongo = async () => {
 152 |   console.log(`${COLORS.yellow}In-memory MongoDB requested. Checking if mongodb-memory-server is available…${COLORS.reset}`)
 153 |   try {
 154 |     const { MongoMemoryServer } = await import('mongodb-memory-server')
 155 |     const mongod = await MongoMemoryServer.create()
 156 |     const uri = mongod.getUri()
 157 |     console.log(`${COLORS.green}In-memory MongoDB instance started at: ${uri}${COLORS.reset}`)
 158 | 
 159 |     process.on('exit', async () => {
 160 |       console.log(`${COLORS.yellow}Stopping in-memory MongoDB server…${COLORS.reset}`)
 161 |       await mongod.stop()
 162 |     })
 163 | 
 164 |     return uri
 165 |   } catch (err) {
 166 |     console.error(`${COLORS.red}Failed to start in-memory MongoDB: ${err.message}${COLORS.reset}`)
 167 |     console.error(`${COLORS.yellow}Install with: npm install mongodb-memory-server --save-dev${COLORS.reset}`)
 168 |     process.exit(1)
 169 |   }
 170 | }
 171 | 
 172 | const connectToMongo = async () => {
 173 |   console.log(`${COLORS.blue}Connecting to MongoDB: ${obfuscateMongoUri(mongoUri)}${COLORS.reset}`)
 174 | 
 175 |   if (!existsSync(MONGODB_LENS_PATH)) {
 176 |     console.error(`${COLORS.red}MongoDB Lens script not found at: ${MONGODB_LENS_PATH}${COLORS.reset}`)
 177 |     process.exit(1)
 178 |   }
 179 | 
 180 |   directMongoClient = new MongoClient(mongoUri, {
 181 |     retryWrites: true
 182 |   })
 183 | 
 184 |   await directMongoClient.connect()
 185 |   console.log(`${COLORS.green}Connected to MongoDB successfully.${COLORS.reset}`)
 186 | }
 187 | 
 188 | const obfuscateMongoUri = uri => {
 189 |   if (!uri || typeof uri !== 'string') return uri
 190 |   try {
 191 |     if (uri.includes('@') && uri.includes('://')) {
 192 |       const parts = uri.split('@')
 193 |       const authPart = parts[0]
 194 |       const restPart = parts.slice(1).join('@')
 195 |       const authIndex = authPart.lastIndexOf('://')
 196 |       if (authIndex !== -1) {
 197 |         const protocol = authPart.substring(0, authIndex + 3)
 198 |         const credentials = authPart.substring(authIndex + 3)
 199 |         if (credentials.includes(':')) {
 200 |           const [username] = credentials.split(':')
 201 |           return `${protocol}${username}:********@${restPart}`
 202 |         }
 203 |       }
 204 |     }
 205 |     return uri
 206 |   } catch (error) {
 207 |     return uri
 208 |   }
 209 | }
 210 | 
 211 | const setupTestEnvironment = async () => {
 212 |   await checkServerCapabilities()
 213 |   testDb = directMongoClient.db(TEST_DB_NAME)
 214 |   await cleanupTestDatabase()
 215 |   await setupTestData()
 216 |   console.log(`${COLORS.blue}Running tests against MongoDB Lens…${COLORS.reset}`)
 217 | }
 218 | 
 219 | const checkServerCapabilities = async () => {
 220 |   try {
 221 |     const adminDb = directMongoClient.db('admin')
 222 | 
 223 |     const replSetStatus = await adminDb.command({ replSetGetStatus: 1 }).catch(() => null)
 224 |     isReplSet = !!replSetStatus
 225 |     console.log(`${COLORS.blue}MongoDB instance ${isReplSet ? 'is' : 'is not'} a replica set.${COLORS.reset}`)
 226 | 
 227 |     const listShards = await adminDb.command({ listShards: 1 }).catch(() => null)
 228 |     isSharded = !!listShards
 229 |     console.log(`${COLORS.blue}MongoDB instance ${isSharded ? 'is' : 'is not'} a sharded cluster.${COLORS.reset}`)
 230 |   } catch (error) {
 231 |     console.log(`${COLORS.yellow}Not a replica set: ${error.message}${COLORS.reset}`)
 232 |   }
 233 | }
 234 | 
 235 | const setupTestData = async () => {
 236 |   try {
 237 |     testCollection = testDb.collection(TEST_COLLECTION_NAME)
 238 |     await createTestDocuments()
 239 |     await createTestIndexes()
 240 |     await createTestGeospatialData()
 241 |     await createTestUser()
 242 |     console.log(`${COLORS.green}Test data setup complete.${COLORS.reset}`)
 243 |   } catch (err) {
 244 |     console.error(`${COLORS.red}Error setting up test data: ${err.message}${COLORS.reset}`)
 245 |   }
 246 | }
 247 | 
 248 | const createTestDocuments = async () => {
 249 |   const testDocuments = Array(50).fill(0).map((_, i) => ({
 250 |     _id: new ObjectId(),
 251 |     name: `Test Document ${i}`,
 252 |     value: i,
 253 |     tags: [`tag${i % 5}`, `category${i % 3}`],
 254 |     isActive: i % 2 === 0,
 255 |     createdAt: new Date(Date.now() - i * 86400000)
 256 |   }))
 257 | 
 258 |   await testCollection.insertMany(testDocuments)
 259 | 
 260 |   const anotherCollection = testDb.collection(ANOTHER_TEST_COLLECTION)
 261 |   await anotherCollection.insertMany([
 262 |     { name: 'Test 1', value: 10 },
 263 |     { name: 'Test 2', value: 20 }
 264 |   ])
 265 | 
 266 |   await testCollection.insertOne({
 267 |     name: 'Date Test',
 268 |     startDate: new Date('2023-01-01'),
 269 |     endDate: new Date('2023-12-31')
 270 |   })
 271 | }
 272 | 
 273 | const createTestIndexes = async () => {
 274 |   await testCollection.createIndex({ name: 1 })
 275 |   await testCollection.createIndex({ value: -1 })
 276 |   await testCollection.createIndex({ tags: 1 })
 277 |   await testCollection.createIndex({ name: 'text', tags: 'text' })
 278 |   await testCollection.createIndex({ location: '2dsphere' })
 279 | }
 280 | 
 281 | const createTestGeospatialData = async () => {
 282 |   await testCollection.insertMany([
 283 |     {
 284 |       name: 'Geo Test 1',
 285 |       location: { type: 'Point', coordinates: [-73.9857, 40.7484] }
 286 |     },
 287 |     {
 288 |       name: 'Geo Test 2',
 289 |       location: { type: 'Point', coordinates: [-118.2437, 34.0522] }
 290 |     }
 291 |   ])
 292 | }
 293 | 
 294 | const createTestUser = async () => {
 295 |   try {
 296 |     await testDb.command({
 297 |       createUser: 'testuser',
 298 |       pwd: 'testpassword',
 299 |       roles: [{ role: 'read', db: TEST_DB_NAME }]
 300 |     })
 301 |   } catch (e) {
 302 |     console.log(`${COLORS.yellow}Could not create test user: ${e.message}${COLORS.reset}`)
 303 |   }
 304 | }
 305 | 
 306 | const runTest = async (name, testFn) => {
 307 |   stats.total++
 308 |   console.log(`${COLORS.blue}Running test: ${name}${COLORS.reset}`)
 309 |   try {
 310 |     const startTime = Date.now()
 311 |     await testFn()
 312 |     const duration = Date.now() - startTime
 313 |     console.log(`${COLORS.green}✓ PASS: ${name} (${duration}ms)${COLORS.reset}`)
 314 |     stats.passed++
 315 |   } catch (err) {
 316 |     console.error(`${COLORS.red}✗ FAIL: ${name} - ${err.message}${COLORS.reset}`)
 317 |     console.error(`${COLORS.red}Stack: ${err.stack}${COLORS.reset}`)
 318 |     stats.failed++
 319 |   }
 320 | }
 321 | 
 322 | const skipTest = (name, reason) => {
 323 |   stats.total++
 324 |   stats.skipped++
 325 |   console.log(`${COLORS.yellow}⚠ SKIP: ${name} - ${reason}${COLORS.reset}`)
 326 | }
 327 | 
 328 | const startLensServer = async () => {
 329 |   console.log('Starting MongoDB Lens server…')
 330 | 
 331 |   const env = {
 332 |     ...process.env,
 333 |     CONFIG_MONGO_URI: mongoUri,
 334 |     CONFIG_LOG_LEVEL: isDebugging ? 'verbose' : 'info',
 335 |   }
 336 | 
 337 |   if (testConfig.disableTokens) {
 338 |     env.CONFIG_DISABLE_DESTRUCTIVE_OPERATION_TOKENS = 'true'
 339 |   }
 340 | 
 341 |   lensProcess = spawn('node', [MONGODB_LENS_PATH], {
 342 |     env: env,
 343 |     stdio: ['pipe', 'pipe', 'pipe']
 344 |   })
 345 | 
 346 |   setupResponseHandling()
 347 |   return waitForServerStart()
 348 | }
 349 | 
 350 | const setupResponseHandling = () => {
 351 |   lensProcess.stdout.on('data', data => {
 352 |     const response = data.toString().trim()
 353 |     try {
 354 |       const parsed = JSON.parse(response)
 355 |       if (parsed.id && responseHandlers.has(parsed.id)) {
 356 |         const handler = responseHandlers.get(parsed.id)
 357 |         responseHandlers.delete(parsed.id)
 358 |         handler.resolve(parsed)
 359 |       }
 360 |     } catch (e) {
 361 |       console.error(`Parse error: ${e.message}`)
 362 |       console.error(`Response was: ${response.substring(0, 200)}${response.length > 200 ? '…' : ''}`)
 363 |     }
 364 |   })
 365 | 
 366 |   lensProcess.stderr.on('data', data => {
 367 |     const output = data.toString().trim()
 368 |     if (isDebugging)
 369 |       console.log(`${COLORS.gray}[SERVER] ${output.split('\n').join(`\n[SERVER] `)}${COLORS.reset}`)
 370 |   })
 371 | }
 372 | 
 373 | const waitForServerStart = () => {
 374 |   return new Promise((resolve, reject) => {
 375 |     const handler = data => {
 376 |       if (data.toString().includes('MongoDB Lens server running.')) {
 377 |         lensProcess.stderr.removeListener('data', handler)
 378 |         console.log('MongoDB Lens server started successfully.')
 379 |         resolve()
 380 |       }
 381 |     }
 382 | 
 383 |     lensProcess.stderr.on('data', handler)
 384 |     setTimeout(() => reject(new Error('Server startup timed out')), testConfig.serverStartupTimeout)
 385 |   })
 386 | }
 387 | 
 388 | const runLensCommand = async ({ command, params = {} }) => {
 389 |   if (!lensProcess) await startLensServer()
 390 | 
 391 |   const requestId = nextRequestId++
 392 |   const { method, methodParams } = mapToLensMethod(command, params)
 393 | 
 394 |   return new Promise((resolve, reject) => {
 395 |     const request = {
 396 |       jsonrpc: '2.0',
 397 |       id: requestId,
 398 |       method: method,
 399 |       params: methodParams
 400 |     }
 401 | 
 402 |     responseHandlers.set(requestId, { resolve, reject })
 403 | 
 404 |     if (isDebugging) console.log(`Sending request #${requestId}:`, JSON.stringify(request))
 405 | 
 406 |     lensProcess.stdin.write(JSON.stringify(request) + '\n')
 407 | 
 408 |     setTimeout(() => {
 409 |       if (responseHandlers.has(requestId)) {
 410 |         responseHandlers.delete(requestId)
 411 |         reject(new Error(`Request ${method} timed out after ${testConfig.requestTimeout/1000} seconds`))
 412 |       }
 413 |     }, testConfig.requestTimeout)
 414 |   })
 415 | }
 416 | 
 417 | const mapToLensMethod = (command, params) => {
 418 |   let method, methodParams
 419 | 
 420 |   switch(command) {
 421 |     case 'mcp.resource.get':
 422 |       method = 'resources/read'
 423 |       methodParams = { uri: params.uri }
 424 |       break
 425 |     case 'mcp.tool.invoke':
 426 |       method = 'tools/call'
 427 |       methodParams = {
 428 |         name: params.name,
 429 |         arguments: params.args || {}
 430 |       }
 431 |       break
 432 |     case 'mcp.prompt.start':
 433 |       method = 'prompts/get'
 434 |       methodParams = {
 435 |         name: params.name,
 436 |         arguments: params.args || {}
 437 |       }
 438 |       break
 439 |     case 'initialize':
 440 |       method = 'initialize'
 441 |       methodParams = params
 442 |       break
 443 |     default:
 444 |       method = command
 445 |       methodParams = params
 446 |   }
 447 | 
 448 |   return { method, methodParams }
 449 | }
 450 | 
 451 | const useTestDatabase = async () => {
 452 |   await runLensCommand({
 453 |     command: 'mcp.tool.invoke',
 454 |     params: {
 455 |       name: 'use-database',
 456 |       args: {
 457 |         database: TEST_DB_NAME
 458 |       }
 459 |     }
 460 |   })
 461 | }
 462 | 
 463 | const handleDestructiveOperationToken = async (tokenResponse, toolName, params) => {
 464 |   if (testConfig.disableTokens) {
 465 |     return assertToolSuccess(tokenResponse, 'has been permanently deleted')
 466 |   }
 467 | 
 468 |   assert(tokenResponse?.result?.content[0].text.includes('Confirmation code:'), 'Confirmation message not found')
 469 |   const tokenMatch = tokenResponse.result.content[0].text.match(/Confirmation code:\s+(\d+)/)
 470 |   assert(tokenMatch && tokenMatch[1], 'Confirmation code not found in text')
 471 | 
 472 |   const token = tokenMatch[1]
 473 |   const completeResponse = await runLensCommand({
 474 |     command: 'mcp.tool.invoke',
 475 |     params: {
 476 |       name: toolName,
 477 |       args: {
 478 |         ...params,
 479 |         token
 480 |       }
 481 |     }
 482 |   })
 483 | 
 484 |   return assertToolSuccess(completeResponse, 'has been permanently deleted')
 485 | }
 486 | 
 487 | const assertToolSuccess = (response, successIndicator) => {
 488 |   assert(response?.result?.content, 'No content in response')
 489 |   assert(Array.isArray(response.result.content), 'Content not an array')
 490 |   assert(response.result.content.length > 0, 'Empty content array')
 491 |   assert(response.result.content[0].text.includes(successIndicator), `Success message not found: "${successIndicator}"`)
 492 |   return response
 493 | }
 494 | 
 495 | const assertResourceSuccess = (response, successIndicator) => {
 496 |   assert(response?.result?.contents, 'No contents in response')
 497 |   assert(Array.isArray(response.result.contents), 'Contents not an array')
 498 |   assert(response.result.contents.length > 0, 'Empty contents array')
 499 |   assert(response.result.contents[0].text.includes(successIndicator), `Success message not found: "${successIndicator}"`)
 500 |   return response
 501 | }
 502 | 
 503 | const assertPromptSuccess = (response, successIndicator) => {
 504 |   assert(response?.result?.messages, 'No messages in response')
 505 |   assert(Array.isArray(response.result.messages), 'Messages not an array')
 506 |   assert(response.result.messages.length > 0, 'Empty messages array')
 507 |   assert(response.result.messages[0].content.text.includes(successIndicator), `Success message not found: "${successIndicator}"`)
 508 |   return response
 509 | }
 510 | 
 511 | const assert = (condition, message, context = null) => {
 512 |   if (condition) return
 513 |   if (context) console.error('CONTEXT:', JSON.stringify(context, null, 2))
 514 |   throw new Error(message || 'Assertion failed')
 515 | }
 516 | 
 517 | const testConnectMongodbTool = async () => {
 518 |   const response = await runLensCommand({
 519 |     command: 'mcp.tool.invoke',
 520 |     params: {
 521 |       name: 'connect-mongodb',
 522 |       args: {
 523 |         uri: mongoUri,
 524 |         validateConnection: 'true'
 525 |       }
 526 |     }
 527 |   })
 528 | 
 529 |   assertToolSuccess(response, 'Successfully connected')
 530 | }
 531 | 
 532 | const testConnectOriginalTool = async () => {
 533 |   await runLensCommand({
 534 |     command: 'mcp.tool.invoke',
 535 |     params: {
 536 |       name: 'connect-mongodb',
 537 |       args: {
 538 |         uri: mongoUri,
 539 |         validateConnection: 'true'
 540 |       }
 541 |     }
 542 |   })
 543 | 
 544 |   const response = await runLensCommand({
 545 |     command: 'mcp.tool.invoke',
 546 |     params: {
 547 |       name: 'connect-original',
 548 |       args: {
 549 |         validateConnection: 'true'
 550 |       }
 551 |     }
 552 |   })
 553 | 
 554 |   assertToolSuccess(response, 'Successfully connected')
 555 | }
 556 | 
 557 | const testAddConnectionAliasTool = async () => {
 558 |   const aliasName = 'test_alias'
 559 |   const response = await runLensCommand({
 560 |     command: 'mcp.tool.invoke',
 561 |     params: {
 562 |       name: 'add-connection-alias',
 563 |       args: {
 564 |         alias: aliasName,
 565 |         uri: mongoUri
 566 |       }
 567 |     }
 568 |   })
 569 | 
 570 |   assertToolSuccess(response, `Successfully added connection alias '${aliasName}'`)
 571 | }
 572 | 
 573 | const testListConnectionsTool = async () => {
 574 |   const aliasName = 'test_alias_for_list'
 575 | 
 576 |   await runLensCommand({
 577 |     command: 'mcp.tool.invoke',
 578 |     params: {
 579 |       name: 'add-connection-alias',
 580 |       args: {
 581 |         alias: aliasName,
 582 |         uri: mongoUri
 583 |       }
 584 |     }
 585 |   })
 586 | 
 587 |   const response = await runLensCommand({
 588 |     command: 'mcp.tool.invoke',
 589 |     params: {
 590 |       name: 'list-connections'
 591 |     }
 592 |   })
 593 | 
 594 |   assertToolSuccess(response, aliasName)
 595 | }
 596 | 
 597 | const testListDatabasesTool = async () => {
 598 |   const response = await runLensCommand({
 599 |     command: 'mcp.tool.invoke',
 600 |     params: {
 601 |       name: 'list-databases'
 602 |     }
 603 |   })
 604 | 
 605 |   assertToolSuccess(response, TEST_DB_NAME)
 606 | }
 607 | 
 608 | const testCurrentDatabaseTool = async () => {
 609 |   await useTestDatabase()
 610 | 
 611 |   const response = await runLensCommand({
 612 |     command: 'mcp.tool.invoke',
 613 |     params: {
 614 |       name: 'current-database'
 615 |     }
 616 |   })
 617 | 
 618 |   assertToolSuccess(response, `Current database: ${TEST_DB_NAME}`)
 619 | }
 620 | 
 621 | const testCreateDatabaseTool = async () => {
 622 |   const testDbName = `test_create_db_${Date.now()}`
 623 | 
 624 |   const response = await runLensCommand({
 625 |     command: 'mcp.tool.invoke',
 626 |     params: {
 627 |       name: 'create-database',
 628 |       args: {
 629 |         name: testDbName,
 630 |         switch: 'true',
 631 |         validateName: 'true'
 632 |       }
 633 |     }
 634 |   })
 635 | 
 636 |   assertToolSuccess(response, `Database '${testDbName}' created`)
 637 | 
 638 |   const dbs = await directMongoClient.db('admin').admin().listDatabases()
 639 |   const dbExists = dbs.databases.some(db => db.name === testDbName)
 640 |   assert(dbExists, `Created database '${testDbName}' not found in database list`)
 641 | 
 642 |   await directMongoClient.db(testDbName).dropDatabase()
 643 | }
 644 | 
 645 | const testUseDatabaseTool = async () => {
 646 |   const response = await runLensCommand({
 647 |     command: 'mcp.tool.invoke',
 648 |     params: {
 649 |       name: 'use-database',
 650 |       args: {
 651 |         database: TEST_DB_NAME
 652 |       }
 653 |     }
 654 |   })
 655 | 
 656 |   assertToolSuccess(response, `Switched to database: ${TEST_DB_NAME}`)
 657 | }
 658 | 
 659 | const testDropDatabaseTool = async () => {
 660 |   const testDbToDrop = `test_drop_db_${Date.now()}`
 661 | 
 662 |   await directMongoClient.db(testDbToDrop).collection('test').insertOne({ test: 1 })
 663 | 
 664 |   await runLensCommand({
 665 |     command: 'mcp.tool.invoke',
 666 |     params: {
 667 |       name: 'use-database',
 668 |       args: {
 669 |         database: testDbToDrop
 670 |       }
 671 |     }
 672 |   })
 673 | 
 674 |   const tokenResponse = await runLensCommand({
 675 |     command: 'mcp.tool.invoke',
 676 |     params: {
 677 |       name: 'drop-database',
 678 |       args: {
 679 |         name: testDbToDrop
 680 |       }
 681 |     }
 682 |   })
 683 | 
 684 |   await handleDestructiveOperationToken(tokenResponse, 'drop-database', { name: testDbToDrop })
 685 | 
 686 |   const dbs = await directMongoClient.db('admin').admin().listDatabases()
 687 |   const dbExists = dbs.databases.some(db => db.name === testDbToDrop)
 688 |   assert(!dbExists, `Dropped database '${testDbToDrop}' still exists`)
 689 | }
 690 | 
 691 | const testCreateUserTool = async () => {
 692 |   const username = `test_user_${Date.now()}`
 693 | 
 694 |   await useTestDatabase()
 695 | 
 696 |   const response = await runLensCommand({
 697 |     command: 'mcp.tool.invoke',
 698 |     params: {
 699 |       name: 'create-user',
 700 |       args: {
 701 |         username: username,
 702 |         password: 'test_password',
 703 |         roles: JSON.stringify([{ role: 'read', db: TEST_DB_NAME }])
 704 |       }
 705 |     }
 706 |   })
 707 | 
 708 |   assertToolSuccess(response, `User '${username}' created`)
 709 | 
 710 |   const usersInfo = await testDb.command({ usersInfo: username })
 711 |   assert(usersInfo.users.length > 0, `Created user '${username}' not found`)
 712 | }
 713 | 
 714 | const testDropUserTool = async () => {
 715 |   const username = `test_drop_user_${Date.now()}`
 716 | 
 717 |   await useTestDatabase()
 718 | 
 719 |   await runLensCommand({
 720 |     command: 'mcp.tool.invoke',
 721 |     params: {
 722 |       name: 'create-user',
 723 |       args: {
 724 |         username: username,
 725 |         password: 'test_password',
 726 |         roles: JSON.stringify([{ role: 'read', db: TEST_DB_NAME }])
 727 |       }
 728 |     }
 729 |   })
 730 | 
 731 |   const tokenResponse = await runLensCommand({
 732 |     command: 'mcp.tool.invoke',
 733 |     params: {
 734 |       name: 'drop-user',
 735 |       args: {
 736 |         username: username
 737 |       }
 738 |     }
 739 |   })
 740 | 
 741 |   let dropped = false
 742 | 
 743 |   if (testConfig.disableTokens) {
 744 |     assertToolSuccess(tokenResponse, 'dropped successfully')
 745 |     dropped = true
 746 |   } else {
 747 |     assert(tokenResponse?.result?.content[0].text.includes('Confirmation code:'), 'Confirmation message not found')
 748 | 
 749 |     const tokenMatch = tokenResponse.result.content[0].text.match(/Confirmation code:\s+(\d+)/)
 750 |     assert(tokenMatch && tokenMatch[1], 'Confirmation code not found in text')
 751 | 
 752 |     const token = tokenMatch[1]
 753 | 
 754 |     const dropResponse = await runLensCommand({
 755 |       command: 'mcp.tool.invoke',
 756 |       params: {
 757 |         name: 'drop-user',
 758 |         args: {
 759 |           username: username,
 760 |           token
 761 |         }
 762 |       }
 763 |     })
 764 | 
 765 |     assertToolSuccess(dropResponse, 'dropped successfully')
 766 |     dropped = true
 767 |   }
 768 | 
 769 |   if (dropped) {
 770 |     const usersInfo = await testDb.command({ usersInfo: username })
 771 |     assert(usersInfo.users.length === 0, `Dropped user '${username}' still exists`)
 772 |   }
 773 | }
 774 | 
 775 | const testListCollectionsTool = async () => {
 776 |   await useTestDatabase()
 777 | 
 778 |   const response = await runLensCommand({
 779 |     command: 'mcp.tool.invoke',
 780 |     params: {
 781 |       name: 'list-collections'
 782 |     }
 783 |   })
 784 | 
 785 |   assertToolSuccess(response, TEST_COLLECTION_NAME)
 786 | }
 787 | 
 788 | const testCreateCollectionTool = async () => {
 789 |   const collectionName = `test_create_coll_${Date.now()}`
 790 | 
 791 |   await useTestDatabase()
 792 | 
 793 |   const response = await runLensCommand({
 794 |     command: 'mcp.tool.invoke',
 795 |     params: {
 796 |       name: 'create-collection',
 797 |       args: {
 798 |         name: collectionName,
 799 |         options: JSON.stringify({})
 800 |       }
 801 |     }
 802 |   })
 803 | 
 804 |   assertToolSuccess(response, `Collection '${collectionName}' created`)
 805 | 
 806 |   const collections = await testDb.listCollections().toArray()
 807 |   const collExists = collections.some(coll => coll.name === collectionName)
 808 |   assert(collExists, `Created collection '${collectionName}' not found`)
 809 | }
 810 | 
 811 | const testDropCollectionTool = async () => {
 812 |   const collectionName = `test_drop_coll_${Date.now()}`
 813 | 
 814 |   await useTestDatabase()
 815 | 
 816 |   await runLensCommand({
 817 |     command: 'mcp.tool.invoke',
 818 |     params: {
 819 |       name: 'create-collection',
 820 |       args: {
 821 |         name: collectionName,
 822 |         options: JSON.stringify({})
 823 |       }
 824 |     }
 825 |   })
 826 | 
 827 |   const tokenResponse = await runLensCommand({
 828 |     command: 'mcp.tool.invoke',
 829 |     params: {
 830 |       name: 'drop-collection',
 831 |       args: {
 832 |         name: collectionName
 833 |       }
 834 |     }
 835 |   })
 836 | 
 837 |   await handleDestructiveOperationToken(tokenResponse, 'drop-collection', { name: collectionName })
 838 | 
 839 |   const collections = await testDb.listCollections().toArray()
 840 |   const collExists = collections.some(coll => coll.name === collectionName)
 841 |   assert(!collExists, `Dropped collection '${collectionName}' still exists`)
 842 | }
 843 | 
 844 | const testRenameCollectionTool = async () => {
 845 |   const oldName = `test_rename_old_${Date.now()}`
 846 |   const newName = `test_rename_new_${Date.now()}`
 847 | 
 848 |   await useTestDatabase()
 849 | 
 850 |   await runLensCommand({
 851 |     command: 'mcp.tool.invoke',
 852 |     params: {
 853 |       name: 'create-collection',
 854 |       args: {
 855 |         name: oldName,
 856 |         options: JSON.stringify({})
 857 |       }
 858 |     }
 859 |   })
 860 | 
 861 |   const response = await runLensCommand({
 862 |     command: 'mcp.tool.invoke',
 863 |     params: {
 864 |       name: 'rename-collection',
 865 |       args: {
 866 |         oldName: oldName,
 867 |         newName: newName,
 868 |         dropTarget: 'false'
 869 |       }
 870 |     }
 871 |   })
 872 | 
 873 |   assertToolSuccess(response, `renamed to '${newName}'`)
 874 | 
 875 |   const collections = await testDb.listCollections().toArray()
 876 |   const oldExists = collections.some(coll => coll.name === oldName)
 877 |   const newExists = collections.some(coll => coll.name === newName)
 878 |   assert(!oldExists, `Old collection '${oldName}' still exists`)
 879 |   assert(newExists, `New collection '${newName}' not found`)
 880 | }
 881 | 
 882 | const testValidateCollectionTool = async () => {
 883 |   await useTestDatabase()
 884 | 
 885 |   const response = await runLensCommand({
 886 |     command: 'mcp.tool.invoke',
 887 |     params: {
 888 |       name: 'validate-collection',
 889 |       args: {
 890 |         collection: TEST_COLLECTION_NAME,
 891 |         full: 'false'
 892 |       }
 893 |     }
 894 |   })
 895 | 
 896 |   assertToolSuccess(response, 'Validation Results')
 897 | 
 898 |   const content = response.result.content[0].text
 899 |   assert(content.includes('Collection'), 'Collection name not found')
 900 |   assert(content.includes('Valid:') || content.includes('Records Validated:'), 'Validation results not found')
 901 | }
 902 | 
 903 | const testDistinctValuesTool = async () => {
 904 |   await useTestDatabase()
 905 | 
 906 |   const response = await runLensCommand({
 907 |     command: 'mcp.tool.invoke',
 908 |     params: {
 909 |       name: 'distinct-values',
 910 |       args: {
 911 |         collection: TEST_COLLECTION_NAME,
 912 |         field: 'tags',
 913 |         filter: '{}'
 914 |       }
 915 |     }
 916 |   })
 917 | 
 918 |   assertToolSuccess(response, 'Distinct values for field')
 919 | 
 920 |   const content = response.result.content[0].text
 921 |   assert(content.includes('tag') || content.includes('category'), 'Expected distinct tag values not found')
 922 | }
 923 | 
 924 | const testFindDocumentsTool = async () => {
 925 |   await useTestDatabase()
 926 | 
 927 |   const response = await runLensCommand({
 928 |     command: 'mcp.tool.invoke',
 929 |     params: {
 930 |       name: 'find-documents',
 931 |       args: {
 932 |         collection: TEST_COLLECTION_NAME,
 933 |         filter: '{"value": {"$lt": 10}}',
 934 |         limit: 5
 935 |       }
 936 |     }
 937 |   })
 938 | 
 939 |   const content = response.result.content[0].text
 940 |   assert(content.includes('"value":'), 'Value field not found')
 941 |   assert(content.includes('"name":'), 'Name field not found')
 942 | }
 943 | 
 944 | const testCountDocumentsTool = async () => {
 945 |   await useTestDatabase()
 946 | 
 947 |   const response = await runLensCommand({
 948 |     command: 'mcp.tool.invoke',
 949 |     params: {
 950 |       name: 'count-documents',
 951 |       args: {
 952 |         collection: TEST_COLLECTION_NAME,
 953 |         filter: '{"isActive": true}'
 954 |       }
 955 |     }
 956 |   })
 957 | 
 958 |   assertToolSuccess(response, 'Count:')
 959 |   assert(/Count: \d+ document/.test(response.result.content[0].text), 'Count number not found')
 960 | }
 961 | 
 962 | const testInsertDocumentTool = async () => {
 963 |   await useTestDatabase()
 964 | 
 965 |   const testDoc = {
 966 |     name: 'Test Insert',
 967 |     value: 999,
 968 |     tags: ['test', 'insert'],
 969 |     isActive: true,
 970 |     createdAt: new Date().toISOString()
 971 |   }
 972 | 
 973 |   const response = await runLensCommand({
 974 |     command: 'mcp.tool.invoke',
 975 |     params: {
 976 |       name: 'insert-document',
 977 |       args: {
 978 |         collection: TEST_COLLECTION_NAME,
 979 |         document: JSON.stringify(testDoc)
 980 |       }
 981 |     }
 982 |   })
 983 | 
 984 |   assertToolSuccess(response, 'inserted successfully')
 985 | 
 986 |   const countResponse = await runLensCommand({
 987 |     command: 'mcp.tool.invoke',
 988 |     params: {
 989 |       name: 'count-documents',
 990 |       args: {
 991 |         collection: TEST_COLLECTION_NAME,
 992 |         filter: '{"value": 999}'
 993 |       }
 994 |     }
 995 |   })
 996 | 
 997 |   assert(countResponse.result.content[0].text.includes('Count: 1'), 'Inserted document not found')
 998 | }
 999 | 
1000 | const testUpdateDocumentTool = async () => {
1001 |   await useTestDatabase()
1002 | 
1003 |   const testDoc = {
1004 |     name: 'Update Test',
1005 |     value: 888,
1006 |     tags: ['update'],
1007 |     isActive: true
1008 |   }
1009 | 
1010 |   await runLensCommand({
1011 |     command: 'mcp.tool.invoke',
1012 |     params: {
1013 |       name: 'insert-document',
1014 |       args: {
1015 |         collection: TEST_COLLECTION_NAME,
1016 |         document: JSON.stringify(testDoc)
1017 |       }
1018 |     }
1019 |   })
1020 | 
1021 |   const response = await runLensCommand({
1022 |     command: 'mcp.tool.invoke',
1023 |     params: {
1024 |       name: 'update-document',
1025 |       args: {
1026 |         collection: TEST_COLLECTION_NAME,
1027 |         filter: '{"value": 888}',
1028 |         update: '{"$set": {"name": "Updated Test", "tags": ["updated"]}}'
1029 |       }
1030 |     }
1031 |   })
1032 | 
1033 |   assertToolSuccess(response, 'Matched:')
1034 |   assert(response.result.content[0].text.includes('Modified:'), 'Modified count not found')
1035 | 
1036 |   const findResponse = await runLensCommand({
1037 |     command: 'mcp.tool.invoke',
1038 |     params: {
1039 |       name: 'find-documents',
1040 |       args: {
1041 |         collection: TEST_COLLECTION_NAME,
1042 |         filter: '{"value": 888}'
1043 |       }
1044 |     }
1045 |   })
1046 | 
1047 |   const responseText = findResponse.result.content[0].text
1048 |   assert(responseText.includes('"name"') && responseText.includes('Updated Test'), 'Updated name not found')
1049 |   assert(responseText.includes('"tags"') && responseText.includes('updated'), 'Updated tags not found')
1050 | }
1051 | 
1052 | const testDeleteDocumentTool = async () => {
1053 |   await useTestDatabase()
1054 | 
1055 |   const testDoc = {
1056 |     name: 'Delete Test',
1057 |     value: 777,
1058 |     tags: ['delete'],
1059 |     isActive: true
1060 |   }
1061 | 
1062 |   await runLensCommand({
1063 |     command: 'mcp.tool.invoke',
1064 |     params: {
1065 |       name: 'insert-document',
1066 |       args: {
1067 |         collection: TEST_COLLECTION_NAME,
1068 |         document: JSON.stringify(testDoc)
1069 |       }
1070 |     }
1071 |   })
1072 | 
1073 |   const countBeforeResponse = await runLensCommand({
1074 |     command: 'mcp.tool.invoke',
1075 |     params: {
1076 |       name: 'count-documents',
1077 |       args: {
1078 |         collection: TEST_COLLECTION_NAME,
1079 |         filter: '{"value": 777}'
1080 |       }
1081 |     }
1082 |   })
1083 | 
1084 |   assert(countBeforeResponse.result.content[0].text.includes('Count: 1'), 'Test document not found before delete')
1085 | 
1086 |   const tokenResponse = await runLensCommand({
1087 |     command: 'mcp.tool.invoke',
1088 |     params: {
1089 |       name: 'delete-document',
1090 |       args: {
1091 |         collection: TEST_COLLECTION_NAME,
1092 |         filter: '{"value": 777}',
1093 |         many: 'false'
1094 |       }
1095 |     }
1096 |   })
1097 | 
1098 |   if (testConfig.disableTokens) {
1099 |     assertToolSuccess(tokenResponse, 'Successfully deleted')
1100 |   } else {
1101 |     assert(tokenResponse?.result?.content[0].text.includes('Confirmation code:'), 'Confirmation message not found')
1102 | 
1103 |     const tokenMatch = tokenResponse.result.content[0].text.match(/Confirmation code:\s+(\d+)/)
1104 |     assert(tokenMatch && tokenMatch[1], 'Confirmation code not found in text')
1105 | 
1106 |     const token = tokenMatch[1]
1107 | 
1108 |     const deleteResponse = await runLensCommand({
1109 |       command: 'mcp.tool.invoke',
1110 |       params: {
1111 |         name: 'delete-document',
1112 |         args: {
1113 |           collection: TEST_COLLECTION_NAME,
1114 |           filter: '{"value": 777}',
1115 |           many: 'false',
1116 |           token
1117 |         }
1118 |       }
1119 |     })
1120 | 
1121 |     assertToolSuccess(deleteResponse, 'Successfully deleted')
1122 |   }
1123 | 
1124 |   const countResponse = await runLensCommand({
1125 |     command: 'mcp.tool.invoke',
1126 |     params: {
1127 |       name: 'count-documents',
1128 |       args: {
1129 |         collection: TEST_COLLECTION_NAME,
1130 |         filter: '{"value": 777}'
1131 |       }
1132 |     }
1133 |   })
1134 | 
1135 |   assert(countResponse.result.content[0].text.includes('Count: 0'), 'Document still exists after deletion')
1136 | }
1137 | 
1138 | const testAggregateDataTool = async () => {
1139 |   await useTestDatabase()
1140 | 
1141 |   const pipeline = [
1142 |     { $match: { isActive: true } },
1143 |     { $group: { _id: null, avgValue: { $avg: '$value' }, count: { $sum: 1 } } }
1144 |   ]
1145 | 
1146 |   const response = await runLensCommand({
1147 |     command: 'mcp.tool.invoke',
1148 |     params: {
1149 |       name: 'aggregate-data',
1150 |       args: {
1151 |         collection: TEST_COLLECTION_NAME,
1152 |         pipeline: JSON.stringify(pipeline)
1153 |       }
1154 |     }
1155 |   })
1156 | 
1157 |   const content = response.result.content[0].text
1158 |   assert(content.includes('"avgValue":'), 'Average value not found')
1159 |   assert(content.includes('"count":'), 'Count not found')
1160 | }
1161 | 
1162 | const testCreateIndexTool = async () => {
1163 |   await useTestDatabase()
1164 | 
1165 |   const indexName = `test_index_${Date.now()}`
1166 | 
1167 |   const response = await runLensCommand({
1168 |     command: 'mcp.tool.invoke',
1169 |     params: {
1170 |       name: 'create-index',
1171 |       args: {
1172 |         collection: TEST_COLLECTION_NAME,
1173 |         keys: '{"createdAt": 1}',
1174 |         options: JSON.stringify({ name: indexName })
1175 |       }
1176 |     }
1177 |   })
1178 | 
1179 |   assertToolSuccess(response, 'Index created')
1180 |   assert(response.result.content[0].text.includes(indexName), 'Index name not found')
1181 | }
1182 | 
1183 | const testDropIndexTool = async () => {
1184 |   await useTestDatabase()
1185 | 
1186 |   const indexName = `test_drop_index_${Date.now()}`
1187 | 
1188 |   await runLensCommand({
1189 |     command: 'mcp.tool.invoke',
1190 |     params: {
1191 |       name: 'create-index',
1192 |       args: {
1193 |         collection: TEST_COLLECTION_NAME,
1194 |         keys: '{"testField": 1}',
1195 |         options: JSON.stringify({ name: indexName })
1196 |       }
1197 |     }
1198 |   })
1199 | 
1200 |   const tokenResponse = await runLensCommand({
1201 |     command: 'mcp.tool.invoke',
1202 |     params: {
1203 |       name: 'drop-index',
1204 |       args: {
1205 |         collection: TEST_COLLECTION_NAME,
1206 |         indexName: indexName
1207 |       }
1208 |     }
1209 |   })
1210 | 
1211 |   let dropped = false
1212 | 
1213 |   if (testConfig.disableTokens) {
1214 |     assertToolSuccess(tokenResponse, 'dropped from collection')
1215 |     dropped = true
1216 |   } else {
1217 |     assert(tokenResponse?.result?.content[0].text.includes('Confirmation code:'), 'Confirmation message not found')
1218 | 
1219 |     const tokenMatch = tokenResponse.result.content[0].text.match(/Confirmation code:\s+(\d+)/)
1220 |     assert(tokenMatch && tokenMatch[1], 'Confirmation code not found in text')
1221 | 
1222 |     const token = tokenMatch[1]
1223 | 
1224 |     const dropResponse = await runLensCommand({
1225 |       command: 'mcp.tool.invoke',
1226 |       params: {
1227 |         name: 'drop-index',
1228 |         args: {
1229 |           collection: TEST_COLLECTION_NAME,
1230 |           indexName: indexName,
1231 |           token
1232 |         }
1233 |       }
1234 |     })
1235 | 
1236 |     assertToolSuccess(dropResponse, 'dropped from collection')
1237 |     dropped = true
1238 |   }
1239 | 
1240 |   if (dropped) {
1241 |     const indexes = await testDb.collection(TEST_COLLECTION_NAME).indexes()
1242 |     const indexStillExists = indexes.some(idx => idx.name === indexName)
1243 |     assert(!indexStillExists, `Dropped index '${indexName}' still exists`)
1244 |   }
1245 | }
1246 | 
1247 | const testAnalyzeSchemaTool = async () => {
1248 |   await useTestDatabase()
1249 | 
1250 |   const response = await runLensCommand({
1251 |     command: 'mcp.tool.invoke',
1252 |     params: {
1253 |       name: 'analyze-schema',
1254 |       args: {
1255 |         collection: TEST_COLLECTION_NAME,
1256 |         sampleSize: 20
1257 |       }
1258 |     }
1259 |   })
1260 | 
1261 |   assertToolSuccess(response, 'Schema for')
1262 |   const content = response.result.content[0].text
1263 |   assert(content.includes('name:'), 'Name field not found')
1264 |   assert(content.includes('value:'), 'Value field not found')
1265 |   assert(content.includes('tags:'), 'Tags field not found')
1266 | }
1267 | 
1268 | const testGenerateSchemaValidatorTool = async () => {
1269 |   await useTestDatabase()
1270 | 
1271 |   const response = await runLensCommand({
1272 |     command: 'mcp.tool.invoke',
1273 |     params: {
1274 |       name: 'generate-schema-validator',
1275 |       args: {
1276 |         collection: TEST_COLLECTION_NAME,
1277 |         strictness: 'moderate'
1278 |       }
1279 |     }
1280 |   })
1281 | 
1282 |   assertToolSuccess(response, 'MongoDB JSON Schema Validator')
1283 |   const content = response.result.content[0].text
1284 |   assert(content.includes('$jsonSchema'), 'JSON Schema not found')
1285 |   assert(content.includes('properties'), 'Properties not found')
1286 | }
1287 | 
1288 | const testCompareSchemasTool = async () => {
1289 |   await useTestDatabase()
1290 | 
1291 |   const response = await runLensCommand({
1292 |     command: 'mcp.tool.invoke',
1293 |     params: {
1294 |       name: 'compare-schemas',
1295 |       args: {
1296 |         sourceCollection: TEST_COLLECTION_NAME,
1297 |         targetCollection: ANOTHER_TEST_COLLECTION,
1298 |         sampleSize: 10
1299 |       }
1300 |     }
1301 |   })
1302 | 
1303 |   assertToolSuccess(response, 'Schema Comparison')
1304 |   const content = response.result.content[0].text
1305 |   assert(content.includes('Source Collection'), 'Source info not found')
1306 |   assert(content.includes('Target Collection'), 'Target info not found')
1307 | }
1308 | 
1309 | const testExplainQueryTool = async () => {
1310 |   await useTestDatabase()
1311 | 
1312 |   const response = await runLensCommand({
1313 |     command: 'mcp.tool.invoke',
1314 |     params: {
1315 |       name: 'explain-query',
1316 |       args: {
1317 |         collection: TEST_COLLECTION_NAME,
1318 |         filter: '{"value": {"$gt": 10}}',
1319 |         verbosity: 'queryPlanner'
1320 |       }
1321 |     }
1322 |   })
1323 | 
1324 |   assertToolSuccess(response, 'Query Explanation')
1325 |   assert(response.result.content[0].text.includes('Query Planner'), 'Query planner not found')
1326 | }
1327 | 
1328 | const testAnalyzeQueryPatternsTool = async () => {
1329 |   await useTestDatabase()
1330 | 
1331 |   const response = await runLensCommand({
1332 |     command: 'mcp.tool.invoke',
1333 |     params: {
1334 |       name: 'analyze-query-patterns',
1335 |       args: {
1336 |         collection: TEST_COLLECTION_NAME,
1337 |         duration: 1
1338 |       }
1339 |     }
1340 |   })
1341 | 
1342 |   assertToolSuccess(response, 'Query Pattern Analysis')
1343 | 
1344 |   const content = response.result.content[0].text
1345 |   assert(
1346 |     content.includes('Analysis') ||
1347 |     content.includes('Recommendations') ||
1348 |     content.includes('Queries') ||
1349 |     content.includes('Patterns'),
1350 |     'No analysis content found in response'
1351 |   )
1352 | }
1353 | 
1354 | const testGetStatsTool = async () => {
1355 |   await useTestDatabase()
1356 | 
1357 |   const dbResponse = await runLensCommand({
1358 |     command: 'mcp.tool.invoke',
1359 |     params: {
1360 |       name: 'get-stats',
1361 |       args: {
1362 |         target: 'database'
1363 |       }
1364 |     }
1365 |   })
1366 | 
1367 |   assertToolSuccess(dbResponse, 'Statistics')
1368 | 
1369 |   const collResponse = await runLensCommand({
1370 |     command: 'mcp.tool.invoke',
1371 |     params: {
1372 |       name: 'get-stats',
1373 |       args: {
1374 |         target: 'collection',
1375 |         name: TEST_COLLECTION_NAME
1376 |       }
1377 |     }
1378 |   })
1379 | 
1380 |   assertToolSuccess(collResponse, 'Statistics')
1381 |   assert(collResponse.result.content[0].text.includes('Document Count'), 'Document count not found')
1382 | }
1383 | 
1384 | const testBulkOperationsTool = async () => {
1385 |   await useTestDatabase()
1386 | 
1387 |   const operations = [
1388 |     { insertOne: { document: { name: 'Bulk Insert 1', value: 1001 } } },
1389 |     { insertOne: { document: { name: 'Bulk Insert 2', value: 1002 } } },
1390 |     { updateOne: { filter: { name: 'Bulk Insert 1' }, update: { $set: { updated: true } } } }
1391 |   ]
1392 | 
1393 |   const response = await runLensCommand({
1394 |     command: 'mcp.tool.invoke',
1395 |     params: {
1396 |       name: 'bulk-operations',
1397 |       args: {
1398 |         collection: TEST_COLLECTION_NAME,
1399 |         operations: JSON.stringify(operations),
1400 |         ordered: 'true'
1401 |       }
1402 |     }
1403 |   })
1404 | 
1405 |   assertToolSuccess(response, 'Bulk Operations Results')
1406 |   assert(response.result.content[0].text.includes('Inserted:'), 'Insert count not found')
1407 | }
1408 | 
1409 | const testCreateTimeseriesCollectionTool = async () => {
1410 |   try {
1411 |     const adminDb = directMongoClient.db('admin')
1412 |     const serverInfo = await adminDb.command({ buildInfo: 1 })
1413 |     const version = serverInfo.version.split('.').map(Number)
1414 | 
1415 |     if (version[0] < 5) {
1416 |       return skipTest('create-timeseries Tool', 'MongoDB version 5.0+ required for time series collections')
1417 |     }
1418 |   } catch (e) {
1419 |     return skipTest('create-timeseries Tool', `Could not determine MongoDB version: ${e.message}`)
1420 |   }
1421 | 
1422 |   await useTestDatabase()
1423 | 
1424 |   const collectionName = `timeseries_${Date.now()}`
1425 | 
1426 |   const response = await runLensCommand({
1427 |     command: 'mcp.tool.invoke',
1428 |     params: {
1429 |       name: 'create-timeseries',
1430 |       args: {
1431 |         name: collectionName,
1432 |         timeField: 'timestamp',
1433 |         metaField: 'metadata',
1434 |         granularity: 'seconds'
1435 |       }
1436 |     }
1437 |   })
1438 | 
1439 |   assertToolSuccess(response, 'Time series collection')
1440 |   assert(response.result.content[0].text.includes('created'), 'Created confirmation not found')
1441 | 
1442 |   const collections = await testDb.listCollections({name: collectionName}).toArray()
1443 |   assert(collections.length > 0, `Timeseries collection '${collectionName}' not found`)
1444 |   assert(collections[0].options?.timeseries, 'Collection is not configured as timeseries')
1445 | }
1446 | 
1447 | const testCollationQueryTool = async () => {
1448 |   await useTestDatabase()
1449 | 
1450 |   const collationTestDocs = [
1451 |     { name: 'café', language: 'French', rank: 1 },
1452 |     { name: 'cafe', language: 'English', rank: 2 },
1453 |     { name: 'CAFE', language: 'English', rank: 3 }
1454 |   ]
1455 | 
1456 |   await testDb.collection(TEST_COLLECTION_NAME).insertMany(collationTestDocs)
1457 | 
1458 |   console.log(`${COLORS.blue}Verifying collation test documents were inserted…${COLORS.reset}`)
1459 |   const testDocs = await testDb.collection(TEST_COLLECTION_NAME)
1460 |     .find({ name: { $in: ['café', 'cafe', 'CAFE'] } })
1461 |     .toArray()
1462 |   console.log(`${COLORS.blue}Found ${testDocs.length} collation test documents${COLORS.reset}`)
1463 | 
1464 |   const response = await runLensCommand({
1465 |     command: 'mcp.tool.invoke',
1466 |     params: {
1467 |       name: 'collation-query',
1468 |       args: {
1469 |         collection: TEST_COLLECTION_NAME,
1470 |         filter: '{"name": "cafe"}',
1471 |         locale: 'en',
1472 |         strength: 1,
1473 |         caseLevel: 'false'
1474 |       }
1475 |     }
1476 |   })
1477 | 
1478 |   const responseText = response.result.content[0].text
1479 |   assert(
1480 |     responseText.includes('café') ||
1481 |     responseText.includes('cafe') ||
1482 |     responseText.includes('CAFE') ||
1483 |     responseText.includes('collation') ||
1484 |     responseText.includes('Locale') ||
1485 |     responseText.includes('Found'),
1486 |     'Collation results not found'
1487 |   )
1488 | }
1489 | 
1490 | const testTextSearchTool = async () => {
1491 |   await useTestDatabase()
1492 | 
1493 |   const response = await runLensCommand({
1494 |     command: 'mcp.tool.invoke',
1495 |     params: {
1496 |       name: 'text-search',
1497 |       args: {
1498 |         collection: TEST_COLLECTION_NAME,
1499 |         searchText: 'test',
1500 |         limit: 5
1501 |       }
1502 |     }
1503 |   })
1504 | 
1505 |   const responseText = response.result.content[0].text
1506 |   assert(
1507 |     responseText.includes('Found') ||
1508 |     responseText.includes('No text index found'),
1509 |     'Text search results or index message not found'
1510 |   )
1511 | }
1512 | 
1513 | const testGeoQueryTool = async () => {
1514 |   await useTestDatabase()
1515 | 
1516 |   const geometry = {
1517 |     type: 'Point',
1518 |     coordinates: [-74, 40.7]
1519 |   }
1520 | 
1521 |   const response = await runLensCommand({
1522 |     command: 'mcp.tool.invoke',
1523 |     params: {
1524 |       name: 'geo-query',
1525 |       args: {
1526 |         collection: TEST_COLLECTION_NAME,
1527 |         operator: 'near',
1528 |         field: 'location',
1529 |         geometry: JSON.stringify(geometry),
1530 |         maxDistance: 2000000,
1531 |         limit: 10
1532 |       }
1533 |     }
1534 |   })
1535 | 
1536 |   assert(response?.result?.content, 'No content in response')
1537 |   assert(Array.isArray(response.result.content), 'Content not an array')
1538 |   assert(response.result.content.length > 0, 'Empty content array')
1539 | 
1540 |   const responseText = response.result.content[0].text
1541 |   assert(
1542 |     responseText.includes('coordinates') ||
1543 |     responseText.includes('location') ||
1544 |     responseText.includes('Geo Test'),
1545 |     'Geospatial data not found in results'
1546 |   )
1547 | }
1548 | 
1549 | const testTransactionTool = async () => {
1550 |   if (!isReplSet) {
1551 |     return skipTest('transaction Tool', 'MongoDB not in replica set mode - transactions require replica set')
1552 |   }
1553 | 
1554 |   await useTestDatabase()
1555 | 
1556 |   const operations = [
1557 |     {
1558 |       operation: 'insert',
1559 |       collection: TEST_COLLECTION_NAME,
1560 |       document: { name: 'Transaction Test 1', value: 1 }
1561 |     },
1562 |     {
1563 |       operation: 'insert',
1564 |       collection: TEST_COLLECTION_NAME,
1565 |       document: { name: 'Transaction Test 2', value: 2 }
1566 |     },
1567 |     {
1568 |       operation: 'update',
1569 |       collection: TEST_COLLECTION_NAME,
1570 |       filter: { name: 'Transaction Test 1' },
1571 |       update: { $set: { updated: true } }
1572 |     }
1573 |   ]
1574 | 
1575 |   const response = await runLensCommand({
1576 |     command: 'mcp.tool.invoke',
1577 |     params: {
1578 |       name: 'transaction',
1579 |       args: {
1580 |         operations: JSON.stringify(operations)
1581 |       }
1582 |     }
1583 |   })
1584 | 
1585 |   assert(response?.result?.content, 'No content in response')
1586 |   assert(Array.isArray(response.result.content), 'Content not an array')
1587 |   assert(response.result.content.length > 0, 'Empty content array')
1588 | 
1589 |   const responseText = response.result.content[0].text
1590 |   assert(
1591 |     responseText.includes('Transaction') &&
1592 |     (responseText.includes('Step') || responseText.includes('committed')),
1593 |     'Transaction results not found'
1594 |   )
1595 | }
1596 | 
1597 | const testWatchChangesTool = async () => {
1598 |   if (!isReplSet) {
1599 |     return skipTest('watch-changes Tool', 'MongoDB not in replica set mode - change streams require replica set')
1600 |   }
1601 | 
1602 |   await useTestDatabase()
1603 | 
1604 |   const response = await runLensCommand({
1605 |     command: 'mcp.tool.invoke',
1606 |     params: {
1607 |       name: 'watch-changes',
1608 |       args: {
1609 |         collection: TEST_COLLECTION_NAME,
1610 |         operations: JSON.stringify(['insert', 'update', 'delete']),
1611 |         duration: 2,
1612 |         fullDocument: 'false'
1613 |       }
1614 |     }
1615 |   })
1616 | 
1617 |   assert(response?.result?.content, 'No content in response')
1618 |   assert(Array.isArray(response.result.content), 'Content not an array')
1619 |   assert(response.result.content.length > 0, 'Empty content array')
1620 | 
1621 |   const responseText = response.result.content[0].text
1622 |   assert(
1623 |     responseText.includes('changes detected') ||
1624 |     responseText.includes('No changes detected'),
1625 |     'Change stream results not found'
1626 |   )
1627 | }
1628 | 
1629 | const testGridFSOperationTool = async () => {
1630 |   await useTestDatabase()
1631 | 
1632 |   try {
1633 |     const bucket = new mongodb.GridFSBucket(testDb)
1634 |     const fileContent = Buffer.from('GridFS test file content')
1635 | 
1636 |     const uploadStream = bucket.openUploadStream('test-gridfs-file.txt')
1637 |     uploadStream.write(fileContent)
1638 |     uploadStream.end()
1639 | 
1640 |     await new Promise(resolve => uploadStream.on('finish', resolve))
1641 |   } catch (e) {
1642 |     console.error(`Failed to create GridFS test file: ${e.message}`)
1643 |   }
1644 | 
1645 |   const response = await runLensCommand({
1646 |     command: 'mcp.tool.invoke',
1647 |     params: {
1648 |       name: 'gridfs-operation',
1649 |       args: {
1650 |         operation: 'list',
1651 |         bucket: 'fs',
1652 |         limit: 10
1653 |       }
1654 |     }
1655 |   })
1656 | 
1657 |   assert(response?.result?.content, 'No content in response')
1658 |   assert(Array.isArray(response.result.content), 'Content not an array')
1659 |   assert(response.result.content.length > 0, 'Empty content array')
1660 | 
1661 |   const responseText = response.result.content[0].text
1662 |   assert(
1663 |     responseText.includes('GridFS') ||
1664 |     responseText.includes('Filename') ||
1665 |     responseText.includes('Size:'),
1666 |     'GridFS data not found in response'
1667 |   )
1668 | }
1669 | 
1670 | const testClearCacheTool = async () => {
1671 |   await useTestDatabase()
1672 | 
1673 |   const response = await runLensCommand({
1674 |     command: 'mcp.tool.invoke',
1675 |     params: {
1676 |       name: 'clear-cache',
1677 |       args: {
1678 |         target: 'all'
1679 |       }
1680 |     }
1681 |   })
1682 | 
1683 |   assertToolSuccess(response, 'cleared')
1684 | }
1685 | 
1686 | const testShardStatusTool = async () => {
1687 |   if (!isSharded) {
1688 |     return skipTest('shard-status Tool', 'MongoDB not in sharded cluster mode - sharding features require sharded deployment')
1689 |   }
1690 | 
1691 |   await useTestDatabase()
1692 | 
1693 |   const response = await runLensCommand({
1694 |     command: 'mcp.tool.invoke',
1695 |     params: {
1696 |       name: 'shard-status',
1697 |       args: {
1698 |         target: 'database'
1699 |       }
1700 |     }
1701 |   })
1702 | 
1703 |   assert(response?.result?.content, 'No content in response')
1704 |   assert(Array.isArray(response.result.content), 'Content not an array')
1705 |   assert(response.result.content.length > 0, 'Empty content array')
1706 | 
1707 |   const responseText = response.result.content[0].text
1708 |   assert(
1709 |     responseText.includes('Sharding Status') ||
1710 |     responseText.includes('sharded cluster') ||
1711 |     responseText.includes('Sharding is not enabled') ||
1712 |     responseText.includes('not a sharded cluster') ||
1713 |     responseText.includes('not running with sharding'),
1714 |     'Sharding status or message not found'
1715 |   )
1716 | }
1717 | 
1718 | const testExportDataTool = async () => {
1719 |   await useTestDatabase()
1720 | 
1721 |   const jsonResponse = await runLensCommand({
1722 |     command: 'mcp.tool.invoke',
1723 |     params: {
1724 |       name: 'export-data',
1725 |       args: {
1726 |         collection: TEST_COLLECTION_NAME,
1727 |         filter: '{"value": {"$lt": 10}}',
1728 |         format: 'json',
1729 |         limit: 5
1730 |       }
1731 |     }
1732 |   })
1733 | 
1734 |   assert(jsonResponse?.result?.content, 'No content in JSON export response')
1735 |   assert(Array.isArray(jsonResponse.result.content), 'JSON export content not an array')
1736 |   assert(jsonResponse.result.content.length > 0, 'Empty JSON export content array')
1737 | 
1738 |   console.log(`${COLORS.blue}JSON export response first 100 chars: ${jsonResponse.result.content[0].text.substring(0, 100)}${COLORS.reset}`)
1739 | 
1740 |   const jsonText = jsonResponse.result.content[0].text
1741 |   assert(
1742 |     jsonText.includes('{') && jsonText.includes('}'),
1743 |     'JSON content not found in export'
1744 |   )
1745 | 
1746 |   const csvResponse = await runLensCommand({
1747 |     command: 'mcp.tool.invoke',
1748 |     params: {
1749 |       name: 'export-data',
1750 |       args: {
1751 |         collection: TEST_COLLECTION_NAME,
1752 |         filter: '{"value": {"$lt": 10}}',
1753 |         format: 'csv',
1754 |         fields: 'name,value,isActive',
1755 |         limit: 5
1756 |       }
1757 |     }
1758 |   })
1759 | 
1760 |   assert(csvResponse?.result?.content, 'No content in CSV export response')
1761 |   assert(Array.isArray(csvResponse.result.content), 'CSV export content not an array')
1762 |   assert(csvResponse.result.content.length > 0, 'Empty CSV export content array')
1763 | 
1764 |   console.log(`${COLORS.blue}CSV export response first 100 chars: ${csvResponse.result.content[0].text.substring(0, 100)}${COLORS.reset}`)
1765 | 
1766 |   assert(csvResponse.result.content[0].text.includes('name') &&
1767 |          csvResponse.result.content[0].text.includes('value'),
1768 |          'CSV headers not found')
1769 | }
1770 | 
1771 | const testDatabasesResource = async () => {
1772 |   const response = await runLensCommand({
1773 |     command: 'mcp.resource.get',
1774 |     params: { uri: 'mongodb://databases' }
1775 |   })
1776 | 
1777 |   assertResourceSuccess(response, 'Databases')
1778 |   assert(response.result.contents[0].text.includes(TEST_DB_NAME), 'Test database not found')
1779 | }
1780 | 
1781 | const testCollectionsResource = async () => {
1782 |   await useTestDatabase()
1783 | 
1784 |   const response = await runLensCommand({
1785 |     command: 'mcp.resource.get',
1786 |     params: { uri: 'mongodb://collections' }
1787 |   })
1788 | 
1789 |   assertResourceSuccess(response, `Collections in ${TEST_DB_NAME}`)
1790 |   assert(response.result.contents[0].text.includes(TEST_COLLECTION_NAME), 'Test collection not found')
1791 | }
1792 | 
1793 | const testDatabaseUsersResource = async () => {
1794 |   const response = await runLensCommand({
1795 |     command: 'mcp.resource.get',
1796 |     params: { uri: 'mongodb://database/users' }
1797 |   })
1798 | 
1799 |   assert(response?.result?.contents, 'No contents in response')
1800 |   assert(Array.isArray(response.result.contents), 'Contents not an array')
1801 |   assert(response.result.contents.length > 0, 'Empty contents array')
1802 |   assert(
1803 |     response.result.contents[0].text.includes('Users in database') ||
1804 |     response.result.contents[0].text.includes('Could not retrieve user information'),
1805 |     'Users title or permission message not found'
1806 |   )
1807 | }
1808 | 
1809 | const testDatabaseTriggersResource = async () => {
1810 |   const response = await runLensCommand({
1811 |     command: 'mcp.resource.get',
1812 |     params: { uri: 'mongodb://database/triggers' }
1813 |   })
1814 | 
1815 |   assert(response?.result?.contents, 'No contents in response')
1816 |   assert(Array.isArray(response.result.contents), 'Contents not an array')
1817 |   assert(response.result.contents.length > 0, 'Empty contents array')
1818 |   assert(
1819 |     response.result.contents[0].text.includes('Change Stream') ||
1820 |     response.result.contents[0].text.includes('Trigger'),
1821 |     'Triggers info not found'
1822 |   )
1823 | }
1824 | 
1825 | const testStoredFunctionsResource = async () => {
1826 |   const response = await runLensCommand({
1827 |     command: 'mcp.resource.get',
1828 |     params: { uri: 'mongodb://database/functions' }
1829 |   })
1830 | 
1831 |   assert(response?.result?.contents, 'No contents in response')
1832 |   assert(Array.isArray(response.result.contents), 'Contents not an array')
1833 |   assert(response.result.contents.length > 0, 'Empty contents array')
1834 |   assert(
1835 |     response.result.contents[0].text.includes('Stored Functions') ||
1836 |     response.result.contents[0].text.includes('No stored JavaScript functions'),
1837 |     'Stored functions title or empty message not found'
1838 |   )
1839 | }
1840 | 
1841 | const testCollectionSchemaResource = async () => {
1842 |   await useTestDatabase()
1843 | 
1844 |   const response = await runLensCommand({
1845 |     command: 'mcp.resource.get',
1846 |     params: { uri: `mongodb://collection/${TEST_COLLECTION_NAME}/schema` }
1847 |   })
1848 | 
1849 |   assertResourceSuccess(response, `Schema for '${TEST_COLLECTION_NAME}'`)
1850 |   const content = response.result.contents[0].text
1851 |   assert(content.includes('name:'), 'Name field not found')
1852 |   assert(content.includes('value:'), 'Value field not found')
1853 |   assert(content.includes('tags:'), 'Tags field not found')
1854 | }
1855 | 
1856 | const testCollectionIndexesResource = async () => {
1857 |   await useTestDatabase()
1858 | 
1859 |   const response = await runLensCommand({
1860 |     command: 'mcp.resource.get',
1861 |     params: { uri: `mongodb://collection/${TEST_COLLECTION_NAME}/indexes` }
1862 |   })
1863 | 
1864 |   assertResourceSuccess(response, 'Indexes')
1865 |   const content = response.result.contents[0].text
1866 |   assert(content.includes('name_1'), 'Name index not found')
1867 |   assert(content.includes('value_-1'), 'Value index not found')
1868 | }
1869 | 
1870 | const testCollectionStatsResource = async () => {
1871 |   await useTestDatabase()
1872 | 
1873 |   const response = await runLensCommand({
1874 |     command: 'mcp.resource.get',
1875 |     params: { uri: `mongodb://collection/${TEST_COLLECTION_NAME}/stats` }
1876 |   })
1877 | 
1878 |   assertResourceSuccess(response, 'Statistics')
1879 |   assert(response.result.contents[0].text.includes('Document Count'), 'Document count not found')
1880 | }
1881 | 
1882 | const testCollectionValidationResource = async () => {
1883 |   await useTestDatabase()
1884 | 
1885 |   const response = await runLensCommand({
1886 |     command: 'mcp.resource.get',
1887 |     params: { uri: `mongodb://collection/${TEST_COLLECTION_NAME}/validation` }
1888 |   })
1889 | 
1890 |   assert(response?.result?.contents, 'No contents in response')
1891 |   assert(Array.isArray(response.result.contents), 'Contents not an array')
1892 |   assert(response.result.contents.length > 0, 'Empty contents array')
1893 |   assert(
1894 |     response.result.contents[0].text.includes('Collection Validation Rules') ||
1895 |     response.result.contents[0].text.includes('does not have any validation rules'),
1896 |     'Validation rules title or empty message not found'
1897 |   )
1898 | }
1899 | 
1900 | const testServerStatusResource = async () => {
1901 |   const response = await runLensCommand({
1902 |     command: 'mcp.resource.get',
1903 |     params: { uri: 'mongodb://server/status' }
1904 |   })
1905 | 
1906 |   assertResourceSuccess(response, 'MongoDB Server Status')
1907 |   assert(response.result.contents[0].text.includes('Version'), 'Version info not found')
1908 | }
1909 | 
1910 | const testReplicaStatusResource = async () => {
1911 |   const response = await runLensCommand({
1912 |     command: 'mcp.resource.get',
1913 |     params: { uri: 'mongodb://server/replica' }
1914 |   })
1915 | 
1916 |   assert(response?.result?.contents, 'No contents in response')
1917 |   assert(Array.isArray(response.result.contents), 'Contents not an array')
1918 |   assert(response.result.contents.length > 0, 'Empty contents array')
1919 |   assert(
1920 |     response.result.contents[0].text.includes('Replica Set') ||
1921 |     response.result.contents[0].text.includes('not available'),
1922 |     'Replica set info or message not found'
1923 |   )
1924 | }
1925 | 
1926 | const testPerformanceMetricsResource = async () => {
1927 |   try {
1928 |     const originalTimeout = testConfig.requestTimeout
1929 |     testConfig.requestTimeout = 30000
1930 | 
1931 |     const response = await runLensCommand({
1932 |       command: 'mcp.resource.get',
1933 |       params: { uri: 'mongodb://server/metrics' }
1934 |     })
1935 | 
1936 |     testConfig.requestTimeout = originalTimeout
1937 | 
1938 |     assert(response?.result?.contents, 'No contents in response')
1939 |     assert(Array.isArray(response.result.contents), 'Contents not an array')
1940 |     assert(response.result.contents.length > 0, 'Empty contents array')
1941 |     assert(
1942 |       response.result.contents[0].text.includes('MongoDB Performance Metrics') ||
1943 |       response.result.contents[0].text.includes('Error getting performance metrics'),
1944 |       'Performance metrics title or error message not found'
1945 |     )
1946 |   } catch (error) {
1947 |     console.log(`${COLORS.yellow}Skipping performance metrics test due to complexity: ${error.message}${COLORS.reset}`)
1948 |     stats.skipped++
1949 |     stats.total--
1950 |   }
1951 | }
1952 | 
1953 | const testQueryBuilderPrompt = async () => {
1954 |   await useTestDatabase()
1955 | 
1956 |   const response = await runLensCommand({
1957 |     command: 'mcp.prompt.start',
1958 |     params: {
1959 |       name: 'query-builder',
1960 |       args: {
1961 |         collection: TEST_COLLECTION_NAME,
1962 |         condition: 'active documents with value greater than 20'
1963 |       }
1964 |     }
1965 |   })
1966 | 
1967 |   assertPromptSuccess(response, TEST_COLLECTION_NAME)
1968 |   assert(response.result.messages[0].content.text.includes('active documents with value greater than 20'), 'Condition not found')
1969 | }
1970 | 
1971 | const testAggregationBuilderPrompt = async () => {
1972 |   await useTestDatabase()
1973 | 
1974 |   const response = await runLensCommand({
1975 |     command: 'mcp.prompt.start',
1976 |     params: {
1977 |       name: 'aggregation-builder',
1978 |       args: {
1979 |         collection: TEST_COLLECTION_NAME,
1980 |         goal: 'calculate average value by active status'
1981 |       }
1982 |     }
1983 |   })
1984 | 
1985 |   assertPromptSuccess(response, TEST_COLLECTION_NAME)
1986 |   assert(response.result.messages[0].content.text.includes('calculate average value by active status'), 'Goal not found')
1987 | }
1988 | 
1989 | const testMongoShellPrompt = async () => {
1990 |   const response = await runLensCommand({
1991 |     command: 'mcp.prompt.start',
1992 |     params: {
1993 |       name: 'mongo-shell',
1994 |       args: {
1995 |         operation: 'find documents with specific criteria',
1996 |         details: 'I want to find all documents where the value is greater than 10'
1997 |       }
1998 |     }
1999 |   })
2000 | 
2001 |   assertPromptSuccess(response, 'find documents with specific criteria')
2002 |   assert(response.result.messages[0].content.text.includes('value is greater than 10'), 'Details not found')
2003 | }
2004 | 
2005 | const testSqlToMongodbPrompt = async () => {
2006 |   const response = await runLensCommand({
2007 |     command: 'mcp.prompt.start',
2008 |     params: {
2009 |       name: 'sql-to-mongodb',
2010 |       args: {
2011 |         sqlQuery: 'SELECT * FROM users WHERE age > 25 ORDER BY name ASC LIMIT 10',
2012 |         targetCollection: 'users'
2013 |       }
2014 |     }
2015 |   })
2016 | 
2017 |   assertPromptSuccess(response, 'SQL query')
2018 |   assert(response.result.messages[0].content.text.includes('SELECT * FROM users'), 'SQL statement not found')
2019 | }
2020 | 
2021 | const testSchemaAnalysisPrompt = async () => {
2022 |   await useTestDatabase()
2023 | 
2024 |   const response = await runLensCommand({
2025 |     command: 'mcp.prompt.start',
2026 |     params: {
2027 |       name: 'schema-analysis',
2028 |       args: {
2029 |         collection: TEST_COLLECTION_NAME
2030 |       }
2031 |     }
2032 |   })
2033 | 
2034 |   assertPromptSuccess(response, TEST_COLLECTION_NAME)
2035 |   assert(response.result.messages[0].content.text.includes('schema'), 'Schema analysis not found')
2036 | }
2037 | 
2038 | const testDataModelingPrompt = async () => {
2039 |   const response = await runLensCommand({
2040 |     command: 'mcp.prompt.start',
2041 |     params: {
2042 |       name: 'data-modeling',
2043 |       args: {
2044 |         useCase: 'E-commerce product catalog',
2045 |         requirements: 'Fast product lookup by category, efficient inventory tracking',
2046 |         existingData: 'Currently using SQL with products and categories tables'
2047 |       }
2048 |     }
2049 |   })
2050 | 
2051 |   assertPromptSuccess(response, 'E-commerce product catalog')
2052 |   assert(response.result.messages[0].content.text.includes('Fast product lookup'), 'Requirements not found')
2053 | }
2054 | 
2055 | const testSchemaVersioningPrompt = async () => {
2056 |   await useTestDatabase()
2057 | 
2058 |   const response = await runLensCommand({
2059 |     command: 'mcp.prompt.start',
2060 |     params: {
2061 |       name: 'schema-versioning',
2062 |       args: {
2063 |         collection: TEST_COLLECTION_NAME,
2064 |         currentSchema: 'Documents with name, value, tags fields',
2065 |         plannedChanges: 'Add a new status field, make tags required',
2066 |         migrationConstraints: 'Zero downtime required'
2067 |       }
2068 |     }
2069 |   })
2070 | 
2071 |   assertPromptSuccess(response, TEST_COLLECTION_NAME)
2072 |   assert(response.result.messages[0].content.text.includes('schema versioning'), 'Schema versioning not found')
2073 | }
2074 | 
2075 | const testMultiTenantDesignPrompt = async () => {
2076 |   const response = await runLensCommand({
2077 |     command: 'mcp.prompt.start',
2078 |     params: {
2079 |       name: 'multi-tenant-design',
2080 |       args: {
2081 |         tenantIsolation: 'collection',
2082 |         estimatedTenants: '50',
2083 |         sharedFeatures: 'User profiles, product catalog',
2084 |         tenantSpecificFeatures: 'Orders, custom pricing',
2085 |         scalingPriorities: 'Read-heavy, occasional bulk writes'
2086 |       }
2087 |     }
2088 |   })
2089 | 
2090 |   assertPromptSuccess(response, 'tenant isolation')
2091 |   assert(response.result.messages[0].content.text.includes('collection'), 'Collection isolation not found')
2092 | }
2093 | 
2094 | const testIndexRecommendationPrompt = async () => {
2095 |   await useTestDatabase()
2096 | 
2097 |   const response = await runLensCommand({
2098 |     command: 'mcp.prompt.start',
2099 |     params: {
2100 |       name: 'index-recommendation',
2101 |       args: {
2102 |         collection: TEST_COLLECTION_NAME,
2103 |         queryPattern: 'filtering by createdAt within a date range'
2104 |       }
2105 |     }
2106 |   })
2107 | 
2108 |   assertPromptSuccess(response, TEST_COLLECTION_NAME)
2109 |   assert(response.result.messages[0].content.text.includes('filtering by createdAt'), 'Query pattern not found')
2110 | }
2111 | 
2112 | const testQueryOptimizerPrompt = async () => {
2113 |   await useTestDatabase()
2114 | 
2115 |   const response = await runLensCommand({
2116 |     command: 'mcp.prompt.start',
2117 |     params: {
2118 |       name: 'query-optimizer',
2119 |       args: {
2120 |         collection: TEST_COLLECTION_NAME,
2121 |         query: '{"tags": "tag0", "value": {"$gt": 10}}',
2122 |         performance: 'Currently taking 500ms with 10,000 documents'
2123 |       }
2124 |     }
2125 |   })
2126 | 
2127 |   assertPromptSuccess(response, TEST_COLLECTION_NAME)
2128 |   assert(response.result.messages[0].content.text.includes('$gt'), 'Query operator not found')
2129 | }
2130 | 
2131 | const testSecurityAuditPrompt = async () => {
2132 |   const response = await runLensCommand({
2133 |     command: 'mcp.prompt.start',
2134 |     params: {
2135 |       name: 'security-audit',
2136 |       args: {}
2137 |     }
2138 |   })
2139 | 
2140 |   assertPromptSuccess(response, 'security audit')
2141 | }
2142 | 
2143 | const testBackupStrategyPrompt = async () => {
2144 |   const response = await runLensCommand({
2145 |     command: 'mcp.prompt.start',
2146 |     params: {
2147 |       name: 'backup-strategy',
2148 |       args: {
2149 |         databaseSize: '50GB',
2150 |         uptime: '99.9%',
2151 |         rpo: '1 hour',
2152 |         rto: '4 hours'
2153 |       }
2154 |     }
2155 |   })
2156 | 
2157 |   assertPromptSuccess(response, 'backup')
2158 |   assert(response.result.messages[0].content.text.includes('50GB'), 'Database size not found')
2159 | }
2160 | 
2161 | const testMigrationGuidePrompt = async () => {
2162 |   const response = await runLensCommand({
2163 |     command: 'mcp.prompt.start',
2164 |     params: {
2165 |       name: 'migration-guide',
2166 |       args: {
2167 |         sourceVersion: '4.4',
2168 |         targetVersion: '5.0',
2169 |         features: 'Time series collections, transactions, aggregation'
2170 |       }
2171 |     }
2172 |   })
2173 | 
2174 |   assertPromptSuccess(response, 'migration')
2175 |   const content = response.result.messages[0].content.text
2176 |   assert(content.includes('4.4'), 'Source version not found')
2177 |   assert(content.includes('5.0'), 'Target version not found')
2178 | }
2179 | 
2180 | const testDatabaseHealthCheckPrompt = async () => {
2181 |   try {
2182 |     await useTestDatabase()
2183 | 
2184 |     const testDoc = {
2185 |       name: 'Health Check Test',
2186 |       value: 100,
2187 |       tags: ['test'],
2188 |       isActive: true
2189 |     }
2190 | 
2191 |     await runLensCommand({
2192 |       command: 'mcp.tool.invoke',
2193 |       params: {
2194 |         name: 'insert-document',
2195 |         args: {
2196 |           collection: TEST_COLLECTION_NAME,
2197 |           document: JSON.stringify(testDoc)
2198 |         }
2199 |       }
2200 |     })
2201 | 
2202 |     const originalTimeout = testConfig.requestTimeout
2203 |     testConfig.requestTimeout = 45000
2204 | 
2205 |     console.log(`${COLORS.blue}Running database health check prompt (increased timeout to ${testConfig.requestTimeout/1000}s)${COLORS.reset}`)
2206 | 
2207 |     const response = await runLensCommand({
2208 |       command: 'mcp.prompt.start',
2209 |       params: {
2210 |         name: 'database-health-check',
2211 |         args: {
2212 |           includePerformance: 'false',
2213 |           includeSchema: 'true',
2214 |           includeSecurity: 'false'
2215 |         }
2216 |       }
2217 |     })
2218 | 
2219 |     testConfig.requestTimeout = originalTimeout
2220 | 
2221 |     assert(response?.result?.messages, 'No messages in response')
2222 |     assert(Array.isArray(response.result.messages), 'Messages not an array')
2223 |     assert(response.result.messages.length > 0, 'Empty messages array')
2224 | 
2225 |     const promptText = response.result.messages[0].content.text
2226 |     assert(
2227 |       promptText.includes('health') ||
2228 |       promptText.includes('Health') ||
2229 |       promptText.includes('assessment'),
2230 |       'Health check content not found'
2231 |     )
2232 |   } catch (error) {
2233 |     console.log(`${COLORS.yellow}Skipping database health check prompt test due to complexity: ${error.message}${COLORS.reset}`)
2234 |     stats.skipped++
2235 |     stats.total--
2236 |   }
2237 | }
2238 | 
2239 | const logHeader = (title, margin = 'none') => {
2240 |   if (margin.indexOf('top') !== -1) console.log('')
2241 |   console.log(`${COLORS.cyan}${DIVIDER}${COLORS.reset}`)
2242 |   console.log(`${COLORS.cyan}${title}${COLORS.reset}`)
2243 |   console.log(`${COLORS.cyan}${DIVIDER}${COLORS.reset}`)
2244 |   if (margin.indexOf('bottom') !== -1) console.log('')
2245 | }
2246 | 
2247 | const displayTestSummary = () => {
2248 |   logHeader('Test Summary', 'margin:top,bottom')
2249 |   console.log(`${COLORS.white}Total Tests: ${stats.total}${COLORS.reset}`)
2250 |   console.log(`${COLORS.green}Passed: ${stats.passed}${COLORS.reset}`)
2251 |   console.log(`${COLORS.red}Failed: ${stats.failed}${COLORS.reset}`)
2252 |   console.log(`${COLORS.yellow}Skipped: ${stats.skipped}${COLORS.reset}`)
2253 | 
2254 |   if (stats.failed > 0) {
2255 |     console.error(`${COLORS.red}Some tests failed.${COLORS.reset}`)
2256 |     process.exit(1)
2257 |   } else {
2258 |     console.log(`${COLORS.green}All tests passed!${COLORS.reset}`)
2259 |     process.exit(0)
2260 |   }
2261 | }
2262 | 
2263 | const cleanup = async () => {
2264 |   await cleanupTestDatabase()
2265 |   await directMongoClient.close()
2266 | 
2267 |   if (lensProcess) {
2268 |     lensProcess.kill('SIGKILL')
2269 |     await new Promise(resolve => lensProcess.on('exit', resolve))
2270 |   }
2271 | }
2272 | 
2273 | const cleanupTestDatabase = async () => {
2274 |   try {
2275 |     await testDb.dropDatabase()
2276 |     console.log(`${COLORS.yellow}Test database cleaned up.${COLORS.reset}`)
2277 |   } catch (err) {
2278 |     console.error(`${COLORS.red}Error cleaning up test database: ${err.message}${COLORS.reset}`)
2279 |   }
2280 | }
2281 | 
2282 | process.on('exit', () => {
2283 |   console.log('Exiting test process…')
2284 |   if (lensProcess) {
2285 |     console.log('Shutting down MongoDB Lens server…')
2286 |     lensProcess.kill()
2287 |   }
2288 | })
2289 | 
2290 | let testDb
2291 | let mongoUri
2292 | let testCollection
2293 | let directMongoClient
2294 | let isReplSet = false
2295 | let isSharded = false
2296 | let nextRequestId = 1
2297 | let lensProcess = null
2298 | let responseHandlers = new Map()
2299 | 
2300 | const testFilters = []
2301 | const groupFilters = []
2302 | const patternFilters = []
2303 | const isDebugging = process.env.DEBUG === 'true'
2304 | 
2305 | const DIVIDER = '-'.repeat(30)
2306 | const TEST_DB_NAME = 'mongodb_lens_test'
2307 | const TEST_COLLECTION_NAME = 'test_collection'
2308 | const ANOTHER_TEST_COLLECTION = 'another_collection'
2309 | const MONGODB_LENS_PATH = join(__dirname, 'mongodb-lens.js')
2310 | 
2311 | const stats = {
2312 |   total: 0,
2313 |   passed: 0,
2314 |   failed: 0,
2315 |   skipped: 0
2316 | }
2317 | 
2318 | const COLORS = {
2319 |   red: '\x1b[31m',
2320 |   reset: '\x1b[0m',
2321 |   blue: '\x1b[34m',
2322 |   cyan: '\x1b[36m',
2323 |   gray: '\x1b[90m',
2324 |   green: '\x1b[32m',
2325 |   white: '\x1b[37m',
2326 |   yellow: '\x1b[33m',
2327 |   magenta: '\x1b[35m',
2328 | }
2329 | 
2330 | const testConfig = {
2331 |   requestTimeout: 15000,
2332 |   serverStartupTimeout: 20000,
2333 |   disableTokens: process.env.CONFIG_DISABLE_DESTRUCTIVE_OPERATION_TOKENS === 'true'
2334 | }
2335 | 
2336 | const TEST_GROUPS = [
2337 |   {
2338 |     name: 'Connection Tools',
2339 |     tests: [
2340 |       { name: 'connect-mongodb Tool', fn: testConnectMongodbTool },
2341 |       { name: 'connect-original Tool', fn: testConnectOriginalTool },
2342 |       { name: 'add-connection-alias Tool', fn: testAddConnectionAliasTool },
2343 |       { name: 'list-connections Tool', fn: testListConnectionsTool }
2344 |     ]
2345 |   },
2346 |   {
2347 |     name: 'Database Tools',
2348 |     tests: [
2349 |       { name: 'list-databases Tool', fn: testListDatabasesTool },
2350 |       { name: 'current-database Tool', fn: testCurrentDatabaseTool },
2351 |       { name: 'create-database Tool', fn: testCreateDatabaseTool },
2352 |       { name: 'use-database Tool', fn: testUseDatabaseTool },
2353 |       { name: 'drop-database Tool', fn: testDropDatabaseTool }
2354 |     ]
2355 |   },
2356 |   {
2357 |     name: 'User Tools',
2358 |     tests: [
2359 |       { name: 'create-user Tool', fn: testCreateUserTool },
2360 |       { name: 'drop-user Tool', fn: testDropUserTool }
2361 |     ]
2362 |   },
2363 |   {
2364 |     name: 'Collection Tools',
2365 |     tests: [
2366 |       { name: 'list-collections Tool', fn: testListCollectionsTool },
2367 |       { name: 'create-collection Tool', fn: testCreateCollectionTool },
2368 |       { name: 'drop-collection Tool', fn: testDropCollectionTool },
2369 |       { name: 'rename-collection Tool', fn: testRenameCollectionTool },
2370 |       { name: 'validate-collection Tool', fn: testValidateCollectionTool }
2371 |     ]
2372 |   },
2373 |   {
2374 |     name: 'Document Tools',
2375 |     tests: [
2376 |       { name: 'distinct-values Tool', fn: testDistinctValuesTool },
2377 |       { name: 'find-documents Tool', fn: testFindDocumentsTool },
2378 |       { name: 'count-documents Tool', fn: testCountDocumentsTool },
2379 |       { name: 'insert-document Tool', fn: testInsertDocumentTool },
2380 |       { name: 'update-document Tool', fn: testUpdateDocumentTool },
2381 |       { name: 'delete-document Tool', fn: testDeleteDocumentTool }
2382 |     ]
2383 |   },
2384 |   {
2385 |     name: 'Advanced Tools',
2386 |     tests: [
2387 |       { name: 'aggregate-data Tool', fn: testAggregateDataTool },
2388 |       { name: 'create-index Tool', fn: testCreateIndexTool },
2389 |       { name: 'drop-index Tool', fn: testDropIndexTool },
2390 |       { name: 'analyze-schema Tool', fn: testAnalyzeSchemaTool },
2391 |       { name: 'generate-schema-validator Tool', fn: testGenerateSchemaValidatorTool },
2392 |       { name: 'compare-schemas Tool', fn: testCompareSchemasTool },
2393 |       { name: 'explain-query Tool', fn: testExplainQueryTool },
2394 |       { name: 'analyze-query-patterns Tool', fn: testAnalyzeQueryPatternsTool },
2395 |       { name: 'get-stats Tool', fn: testGetStatsTool },
2396 |       { name: 'bulk-operations Tool', fn: testBulkOperationsTool },
2397 |       { name: 'create-timeseries Tool', fn: testCreateTimeseriesCollectionTool },
2398 |       { name: 'collation-query Tool', fn: testCollationQueryTool },
2399 |       { name: 'text-search Tool', fn: testTextSearchTool },
2400 |       { name: 'geo-query Tool', fn: testGeoQueryTool },
2401 |       { name: 'transaction Tool', fn: testTransactionTool },
2402 |       { name: 'watch-changes Tool', fn: testWatchChangesTool },
2403 |       { name: 'gridfs-operation Tool', fn: testGridFSOperationTool },
2404 |       { name: 'clear-cache Tool', fn: testClearCacheTool },
2405 |       { name: 'shard-status Tool', fn: testShardStatusTool },
2406 |       { name: 'export-data Tool', fn: testExportDataTool }
2407 |     ]
2408 |   },
2409 |   {
2410 |     name: 'Resources',
2411 |     tests: [
2412 |       { name: 'databases Resource', fn: testDatabasesResource },
2413 |       { name: 'collections Resource', fn: testCollectionsResource },
2414 |       { name: 'database-users Resource', fn: testDatabaseUsersResource },
2415 |       { name: 'database-triggers Resource', fn: testDatabaseTriggersResource },
2416 |       { name: 'stored-functions Resource', fn: testStoredFunctionsResource },
2417 |       { name: 'collection-schema Resource', fn: testCollectionSchemaResource },
2418 |       { name: 'collection-indexes Resource', fn: testCollectionIndexesResource },
2419 |       { name: 'collection-stats Resource', fn: testCollectionStatsResource },
2420 |       { name: 'collection-validation Resource', fn: testCollectionValidationResource },
2421 |       { name: 'server-status Resource', fn: testServerStatusResource },
2422 |       { name: 'replica-status Resource', fn: testReplicaStatusResource },
2423 |       { name: 'performance-metrics Resource', fn: testPerformanceMetricsResource }
2424 |     ]
2425 |   },
2426 |   {
2427 |     name: 'Prompts',
2428 |     tests: [
2429 |       { name: 'query-builder Prompt', fn: testQueryBuilderPrompt },
2430 |       { name: 'aggregation-builder Prompt', fn: testAggregationBuilderPrompt },
2431 |       { name: 'mongo-shell Prompt', fn: testMongoShellPrompt },
2432 |       { name: 'sql-to-mongodb Prompt', fn: testSqlToMongodbPrompt },
2433 |       { name: 'schema-analysis Prompt', fn: testSchemaAnalysisPrompt },
2434 |       { name: 'data-modeling Prompt', fn: testDataModelingPrompt },
2435 |       { name: 'schema-versioning Prompt', fn: testSchemaVersioningPrompt },
2436 |       { name: 'multi-tenant-design Prompt', fn: testMultiTenantDesignPrompt },
2437 |       { name: 'index-recommendation Prompt', fn: testIndexRecommendationPrompt },
2438 |       { name: 'query-optimizer Prompt', fn: testQueryOptimizerPrompt },
2439 |       { name: 'security-audit Prompt', fn: testSecurityAuditPrompt },
2440 |       { name: 'backup-strategy Prompt', fn: testBackupStrategyPrompt },
2441 |       { name: 'migration-guide Prompt', fn: testMigrationGuidePrompt },
2442 |       { name: 'database-health-check Prompt', fn: testDatabaseHealthCheckPrompt }
2443 |     ]
2444 |   }
2445 | ]
2446 | 
2447 | runTests().catch(err => {
2448 |   console.error(`${COLORS.red}Test runner error: ${err.message}${COLORS.reset}`)
2449 |   if (lensProcess) lensProcess.kill()
2450 |   process.exit(1)
2451 | })
2452 | 
```
Page 2/3FirstPrevNextLast