mlgym-deploy 3.3.40 → 3.3.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -18,7 +18,7 @@ import crypto from 'crypto';
18
18
  const execAsync = promisify(exec);
19
19
 
20
20
  // Current version of this MCP server - INCREMENT FOR WORKFLOW FIXES
21
- const CURRENT_VERSION = '3.3.40'; // Fix auth.jwt_token -> auth.token consistency bug
21
+ const CURRENT_VERSION = '3.3.42'; // Add deploy_dollie tool for Next.js dev containers with SSH
22
22
  const PACKAGE_NAME = 'mlgym-deploy';
23
23
 
24
24
  // Debug logging configuration - ENABLED BY DEFAULT
@@ -1949,6 +1949,42 @@ async function detectMissingNodeDependencies(projectPath) {
1949
1949
  return null;
1950
1950
  }
1951
1951
 
1952
+ /**
1953
+ * Detect invalid path alias dependencies in package.json
1954
+ * Path aliases like "@/components" should only exist in tsconfig.json, not as npm packages
1955
+ */
1956
+ function detectInvalidPathAliasDependencies(packageJsonPath) {
1957
+ try {
1958
+ const packageJsonContent = fsSync.readFileSync(packageJsonPath, 'utf-8');
1959
+ const packageJson = JSON.parse(packageJsonContent);
1960
+
1961
+ const invalidDeps = [];
1962
+ const allDeps = {
1963
+ ...packageJson.dependencies || {},
1964
+ ...packageJson.devDependencies || {}
1965
+ };
1966
+
1967
+ // Check for path alias patterns like "@/something"
1968
+ for (const [depName, depVersion] of Object.entries(allDeps)) {
1969
+ if (depName.startsWith('@/')) {
1970
+ invalidDeps.push(depName);
1971
+ }
1972
+ }
1973
+
1974
+ if (invalidDeps.length > 0) {
1975
+ return {
1976
+ invalid: true,
1977
+ packages: invalidDeps,
1978
+ autofix: 'remove_path_aliases'
1979
+ };
1980
+ }
1981
+ } catch (err) {
1982
+ log.warning(`MCP >>> Path alias validation failed: ${err.message}`);
1983
+ }
1984
+
1985
+ return null;
1986
+ }
1987
+
1952
1988
  // Validate Dockerfile for Coolify compliance
1953
1989
  function validateDockerfile(content) {
1954
1990
  const lines = content.split('\n');
@@ -3299,6 +3335,51 @@ async function deployProject(args) {
3299
3335
  fsSync.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
3300
3336
  log.success(`MCP >>> Added missing dependencies: ${missingDeps.packages.join(', ')}`);
3301
3337
  }
3338
+
3339
+ // Step 4.6: Check for invalid path alias dependencies
3340
+ log.info('MCP >>> Checking for invalid path alias dependencies...');
3341
+ const invalidDeps = detectInvalidPathAliasDependencies(packageJsonPath);
3342
+ if (invalidDeps && invalidDeps.invalid && invalidDeps.packages.length > 0) {
3343
+ log.warning(`MCP >>> ⚠️ CRITICAL: Invalid path alias dependencies detected!`);
3344
+ log.warning(`MCP >>> The following entries should NOT be in package.json:`);
3345
+ invalidDeps.packages.forEach(pkg => log.warning(`MCP >>> - ${pkg}`));
3346
+ log.warning(`MCP >>> `);
3347
+ log.warning(`MCP >>> ❌ ERROR: npm cannot install packages with names like "@/components"`);
3348
+ log.warning(`MCP >>> These are TypeScript/JavaScript path aliases, not npm packages!`);
3349
+ log.warning(`MCP >>> `);
3350
+ log.warning(`MCP >>> ✅ HOW TO FIX:`);
3351
+ log.warning(`MCP >>> 1. Remove these entries from package.json dependencies/devDependencies`);
3352
+ log.warning(`MCP >>> 2. Keep them ONLY in tsconfig.json or jsconfig.json under "paths":`);
3353
+ log.warning(`MCP >>> {`);
3354
+ log.warning(`MCP >>> "compilerOptions": {`);
3355
+ log.warning(`MCP >>> "paths": { "@/*": ["./*"] }`);
3356
+ log.warning(`MCP >>> }`);
3357
+ log.warning(`MCP >>> }`);
3358
+ log.warning(`MCP >>> `);
3359
+ log.info('MCP >>> Auto-fixing: Removing invalid path alias dependencies from package.json');
3360
+
3361
+ const packageJsonContent = fsSync.readFileSync(packageJsonPath, 'utf-8');
3362
+ const packageJson = JSON.parse(packageJsonContent);
3363
+
3364
+ // Remove invalid dependencies
3365
+ let removedCount = 0;
3366
+ for (const pkg of invalidDeps.packages) {
3367
+ if (packageJson.dependencies && packageJson.dependencies[pkg]) {
3368
+ delete packageJson.dependencies[pkg];
3369
+ removedCount++;
3370
+ }
3371
+ if (packageJson.devDependencies && packageJson.devDependencies[pkg]) {
3372
+ delete packageJson.devDependencies[pkg];
3373
+ removedCount++;
3374
+ }
3375
+ }
3376
+
3377
+ // Create backup and save fixed version
3378
+ fsSync.writeFileSync(packageJsonPath + '.backup', packageJsonContent);
3379
+ fsSync.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
3380
+ log.success(`MCP >>> ✅ Removed ${removedCount} invalid path alias dependencies`);
3381
+ log.info(`MCP >>> Backup saved to: package.json.backup`);
3382
+ }
3302
3383
  }
3303
3384
 
3304
3385
  // Step 5: Create GitLab project and deploy
@@ -3353,6 +3434,205 @@ async function deployProject(args) {
3353
3434
  }
3354
3435
  }
3355
3436
 
3437
+ // ============================================================================
3438
+ // DOLLIE DEPLOYMENT - Next.js Dev Containers with SSH
3439
+ // ============================================================================
3440
+ async function deployDollie(args) {
3441
+ const {
3442
+ project_name,
3443
+ project_description,
3444
+ git_repo_url,
3445
+ git_branch = '',
3446
+ git_token = '',
3447
+ app_port = 3000,
3448
+ email,
3449
+ password
3450
+ } = args;
3451
+
3452
+ log.info('MCP >>> Starting Dollie deployment workflow');
3453
+ log.debug('MCP >>> Arguments:', { project_name, git_repo_url, git_branch, app_port });
3454
+
3455
+ try {
3456
+ // Step 1: Authenticate
3457
+ log.info('MCP >>> STEP 1: Authenticating...');
3458
+ const auth = await loadAuth();
3459
+
3460
+ // If not authenticated and credentials provided, authenticate
3461
+ if (!auth.token && email && password) {
3462
+ const loginResult = await apiRequest('POST', '/api/v1/auth/login', {
3463
+ email,
3464
+ password
3465
+ }, false);
3466
+
3467
+ if (!loginResult.success || !loginResult.data.token) {
3468
+ return {
3469
+ content: [{
3470
+ type: 'text',
3471
+ text: JSON.stringify({
3472
+ status: 'error',
3473
+ message: 'Authentication failed',
3474
+ error: loginResult.error || 'Invalid credentials',
3475
+ next_steps: [
3476
+ 'Provide valid email and password',
3477
+ 'Or ensure you are already logged in'
3478
+ ]
3479
+ }, null, 2)
3480
+ }]
3481
+ };
3482
+ }
3483
+
3484
+ await saveAuth(email, loginResult.data.token);
3485
+ log.success('MCP >>> STEP 1: Authentication successful');
3486
+ } else if (!auth.token) {
3487
+ return {
3488
+ content: [{
3489
+ type: 'text',
3490
+ text: JSON.stringify({
3491
+ status: 'error',
3492
+ message: 'Authentication required',
3493
+ error: 'No authentication token found and no credentials provided',
3494
+ next_steps: [
3495
+ 'Provide email and password in the request',
3496
+ 'Or use mlgym_auth_login first'
3497
+ ]
3498
+ }, null, 2)
3499
+ }]
3500
+ };
3501
+ } else {
3502
+ log.success('MCP >>> STEP 1: Using existing authentication');
3503
+ }
3504
+
3505
+ // Step 2: Get current user info
3506
+ log.info('MCP >>> STEP 2: Getting user information...');
3507
+ const userResult = await apiRequest('GET', '/api/v1/user', null, true);
3508
+
3509
+ if (!userResult.success) {
3510
+ return {
3511
+ content: [{
3512
+ type: 'text',
3513
+ text: JSON.stringify({
3514
+ status: 'error',
3515
+ message: 'Failed to get user information',
3516
+ error: userResult.error
3517
+ }, null, 2)
3518
+ }]
3519
+ };
3520
+ }
3521
+
3522
+ log.success('MCP >>> STEP 2: User information retrieved');
3523
+
3524
+ // Step 3: Generate SSH password
3525
+ log.info('MCP >>> STEP 3: Generating SSH password...');
3526
+ const sshPassword = generateRandomPassword();
3527
+ log.success('MCP >>> STEP 3: SSH password generated');
3528
+
3529
+ // Step 4: Prepare environment variables
3530
+ log.info('MCP >>> STEP 4: Preparing environment variables...');
3531
+ const environmentVariables = {
3532
+ GIT_REPO_URL: git_repo_url,
3533
+ GIT_BRANCH: git_branch,
3534
+ GIT_TOKEN: git_token,
3535
+ SSH_PASSWORD: sshPassword,
3536
+ S5_USERNAME: 'developer',
3537
+ APP_PORT: app_port.toString(),
3538
+ SSH_PORT: '22'
3539
+ };
3540
+ log.success('MCP >>> STEP 4: Environment variables prepared');
3541
+ log.debug('MCP >>> Environment variables:', Object.keys(environmentVariables));
3542
+
3543
+ // Step 5: Create Coolify resource with docker-image
3544
+ log.info('MCP >>> STEP 5: Creating Coolify deployment...');
3545
+ const createResult = await apiRequest('POST', '/api/v1/projects', {
3546
+ name: project_name,
3547
+ description: project_description,
3548
+ repository: '', // Not needed for docker-image deployments
3549
+ branch: '',
3550
+ build_pack: 'docker-image',
3551
+ docker_image: 'code.stratus5.com:5050/dollie/dollie/nextjs-dev:latest',
3552
+ environment_variables: environmentVariables,
3553
+ ports_exposes: app_port.toString()
3554
+ }, true);
3555
+
3556
+ if (!createResult.success) {
3557
+ return {
3558
+ content: [{
3559
+ type: 'text',
3560
+ text: JSON.stringify({
3561
+ status: 'error',
3562
+ message: 'Failed to create Dollie deployment',
3563
+ error: createResult.error,
3564
+ step: 'create_coolify_resource'
3565
+ }, null, 2)
3566
+ }]
3567
+ };
3568
+ }
3569
+
3570
+ log.success('MCP >>> STEP 5: Coolify deployment created');
3571
+
3572
+ // Step 6: Extract deployment details
3573
+ const deployment = createResult.data;
3574
+ const appUuid = deployment.coolify_uuid;
3575
+ const appUrl = deployment.url;
3576
+
3577
+ // Parse URL to extract hostname
3578
+ const urlObj = new URL(appUrl);
3579
+ const hostname = urlObj.hostname;
3580
+
3581
+ // Determine SSH port based on node (typically 2201 for all nodes)
3582
+ const sshPort = 2201;
3583
+
3584
+ log.success('MCP >>> Dollie deployment completed successfully');
3585
+
3586
+ return {
3587
+ content: [{
3588
+ type: 'text',
3589
+ text: JSON.stringify({
3590
+ success: true,
3591
+ deployment: {
3592
+ url: appUrl,
3593
+ ssh: {
3594
+ host: hostname,
3595
+ port: sshPort,
3596
+ username: 'developer',
3597
+ password: sshPassword
3598
+ },
3599
+ status: 'deploying',
3600
+ coolify_uuid: appUuid,
3601
+ project_name: project_name,
3602
+ git_repo: git_repo_url,
3603
+ git_branch: git_branch || 'default',
3604
+ app_port: app_port
3605
+ },
3606
+ next_steps: [
3607
+ `Container is deploying at ${appUrl}`,
3608
+ `SSH access: ssh developer@${hostname} -p ${sshPort}`,
3609
+ `Password: ${sshPassword}`,
3610
+ 'The container will:',
3611
+ ' 1. Clone your git repository',
3612
+ ' 2. Install dependencies (npm/yarn/pnpm)',
3613
+ ' 3. Start Next.js dev server',
3614
+ ' 4. Enable SSH access',
3615
+ 'Wait 2-3 minutes for deployment to complete'
3616
+ ]
3617
+ }, null, 2)
3618
+ }]
3619
+ };
3620
+
3621
+ } catch (error) {
3622
+ log.error('MCP >>> Dollie deployment failed:', error.message);
3623
+ return {
3624
+ content: [{
3625
+ type: 'text',
3626
+ text: JSON.stringify({
3627
+ status: 'error',
3628
+ message: 'Dollie deployment failed',
3629
+ error: error.message
3630
+ }, null, 2)
3631
+ }]
3632
+ };
3633
+ }
3634
+ }
3635
+
3356
3636
  // ============================================================================
3357
3637
  // SIMPLIFIED STATUS CHECK
3358
3638
  // ============================================================================
@@ -4686,6 +4966,55 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
4686
4966
  }
4687
4967
  }
4688
4968
  },
4969
+ {
4970
+ name: 'deploy_dollie',
4971
+ description: 'Deploy a Next.js development container with SSH access for AI-assisted development. Uses the Dollie base image which clones your git repository and runs the dev server. Supports any public git repo or private repos with token authentication.',
4972
+ inputSchema: {
4973
+ type: 'object',
4974
+ properties: {
4975
+ project_name: {
4976
+ type: 'string',
4977
+ description: 'Project name (lowercase alphanumeric with hyphens, e.g., "my-nextjs-app")',
4978
+ pattern: '^[a-z0-9][a-z0-9-]*[a-z0-9]$',
4979
+ minLength: 3
4980
+ },
4981
+ project_description: {
4982
+ type: 'string',
4983
+ description: 'Brief project description (min 10 characters)',
4984
+ minLength: 10
4985
+ },
4986
+ git_repo_url: {
4987
+ type: 'string',
4988
+ description: 'Git repository URL (https://github.com/user/repo.git)',
4989
+ pattern: '^https://.*\\.git$'
4990
+ },
4991
+ git_branch: {
4992
+ type: 'string',
4993
+ description: 'Branch to clone (optional, defaults to repo\'s default branch)'
4994
+ },
4995
+ git_token: {
4996
+ type: 'string',
4997
+ description: 'OAuth token for private repos (optional)'
4998
+ },
4999
+ app_port: {
5000
+ type: 'number',
5001
+ description: 'Next.js port inside container (optional, default: 3000)',
5002
+ default: 3000
5003
+ },
5004
+ email: {
5005
+ type: 'string',
5006
+ description: 'Email for authentication (optional if already authenticated)',
5007
+ pattern: '^[^@]+@[^@]+\\.[^@]+$'
5008
+ },
5009
+ password: {
5010
+ type: 'string',
5011
+ description: 'Password (optional if already authenticated, min 8 characters)',
5012
+ minLength: 8
5013
+ }
5014
+ },
5015
+ required: ['project_name', 'project_description', 'git_repo_url']
5016
+ }
5017
+ },
4689
5018
  {
4690
5019
  name: 'mlgym_help',
4691
5020
  description: 'Display all available MLGym tools with descriptions and usage examples. Use this to discover what operations you can perform.',
@@ -4821,13 +5150,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4821
5150
  result = await rollback(args);
4822
5151
  break;
4823
5152
 
5153
+ case 'deploy_dollie':
5154
+ log.info(`Deploying Dollie development container...`);
5155
+ result = await deployDollie(args);
5156
+ break;
5157
+
4824
5158
  case 'mlgym_help':
4825
5159
  log.info(`Showing help...`);
4826
5160
  result = await showHelp(args);
4827
5161
  break;
4828
5162
 
4829
5163
  default:
4830
- throw new Error(`Unknown tool: ${name}. Available tools: mlgym_deploy, mlgym_status, mlgym_deploy_logs, mlgym_user_create, mlgym_auth_login, mlgym_set_env_vars, mlgym_set_health_check, mlgym_set_domain, mlgym_set_deployment_commands, mlgym_deploy_manual, mlgym_set_options, mlgym_rollback`);
5164
+ throw new Error(`Unknown tool: ${name}. Available tools: mlgym_deploy, mlgym_status, mlgym_deploy_logs, deploy_dollie, mlgym_user_create, mlgym_auth_login, mlgym_set_env_vars, mlgym_set_health_check, mlgym_set_domain, mlgym_set_deployment_commands, mlgym_deploy_manual, mlgym_set_options, mlgym_rollback`);
4831
5165
  }
4832
5166
 
4833
5167
  const duration = Date.now() - startTime;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mlgym-deploy",
3
- "version": "3.3.40",
3
+ "version": "3.3.42",
4
4
  "description": "MCP server for MLGym - Complete deployment management: deploy, configure, monitor, and rollback applications",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -80,6 +80,10 @@
80
80
  "name": "mlgym_rollback",
81
81
  "description": "Rollback to previous deployment"
82
82
  },
83
+ {
84
+ "name": "deploy_dollie",
85
+ "description": "Deploy Next.js development container with SSH access using pre-built Dollie image"
86
+ },
83
87
  {
84
88
  "name": "mlgym_help",
85
89
  "description": "Display all available tools with descriptions and usage examples"
package/tests/README.md CHANGED
@@ -13,7 +13,8 @@ tests/
13
13
  ├── mlgym_auth_logout_test.sh # ✅ 8 tests (auth_logout)
14
14
  ├── mlgym_project_init_test.sh # ✅ 6 tests (project_init)
15
15
  ├── mlgym_projects_list_test.sh # ✅ 6 tests (projects_list)
16
- └── mlgym_projects_get_test.sh # ✅ 7 tests (projects_get)
16
+ ├── mlgym_projects_get_test.sh # ✅ 7 tests (projects_get)
17
+ └── deploy_dollie_test.sh # ✅ 20 tests (deploy_dollie)
17
18
  ```
18
19
 
19
20
  ## Running Tests
@@ -32,7 +33,7 @@ cd mcp-server
32
33
 
33
34
  ## Test Coverage
34
35
 
35
- ### Completed (6 functions, 62 total tests)
36
+ ### Completed (7 functions, 82 total tests)
36
37
  - ✅ **mlgym_auth_login** (15 tests)
37
38
  - Valid/invalid credentials, SQL injection, XSS, buffer overflow, token storage
38
39
  - ✅ **mlgym_user_create** (20 tests)
@@ -45,6 +46,10 @@ cd mcp-server
45
46
  - List projects, user isolation, response format
46
47
  - ✅ **mlgym_projects_get** (7 tests)
47
48
  - Get project details, validation, non-existent projects
49
+ - ✅ **deploy_dollie** (20 tests)
50
+ - Authentication, deployment workflow, public repos, custom ports/branches
51
+ - Required fields, validation, security (SQL injection, XSS, buffer overflow)
52
+ - Response format, SSH password strength
48
53
 
49
54
  ### Remaining (37 functions)
50
55
  See TODO.md for priority order and testing plan.
@@ -0,0 +1,199 @@
1
+ #!/bin/bash
2
+ # Comprehensive test suite for deploy_dollie
3
+ # Tests functional requirements, edge cases, and security
4
+
5
+ set -e
6
+
7
+ PASSED=0
8
+ FAILED=0
9
+
10
+ echo "=== deploy_dollie Comprehensive Test Suite ==="
11
+ echo ""
12
+
13
+ # Helper to run test
14
+ run_test() {
15
+ local test_name="$1"
16
+ local project_name="$2"
17
+ local project_description="$3"
18
+ local git_repo_url="$4"
19
+ local git_branch="$5"
20
+ local app_port="$6"
21
+ local expected_result="$7" # "success" or "error"
22
+
23
+ # Build JSON request
24
+ local branch_field=""
25
+ if [ -n "$git_branch" ]; then
26
+ branch_field=",\"git_branch\":\"$git_branch\""
27
+ fi
28
+
29
+ local port_field=""
30
+ if [ -n "$app_port" ]; then
31
+ port_field=",\"app_port\":$app_port"
32
+ fi
33
+
34
+ local req="{\"jsonrpc\":\"2.0\",\"method\":\"tools/call\",\"params\":{\"name\":\"deploy_dollie\",\"arguments\":{\"project_name\":\"$project_name\",\"project_description\":\"$project_description\",\"git_repo_url\":\"$git_repo_url\"$branch_field$port_field}},\"id\":99}"
35
+ local resp=$(echo "$req" | node index.js 2>/dev/null | tail -1)
36
+
37
+ # Check for deployment details (success) or error message
38
+ local content=$(echo "$resp" | jq -r '.result.content[0].text' 2>/dev/null)
39
+ local success=$(echo "$content" | jq -r '.success' 2>/dev/null)
40
+ local error_msg=$(echo "$content" | jq -r '.message' 2>/dev/null)
41
+
42
+ if [ "$expected_result" = "success" ]; then
43
+ if [ "$success" = "true" ]; then
44
+ local ssh_host=$(echo "$content" | jq -r '.deployment.ssh.host' 2>/dev/null)
45
+ local ssh_password=$(echo "$content" | jq -r '.deployment.ssh.password' 2>/dev/null)
46
+ echo "✅ $test_name (host: $ssh_host, password: ${ssh_password:0:8}...)"
47
+ PASSED=$((PASSED + 1))
48
+ else
49
+ echo "❌ $test_name (expected deployment success, got: $error_msg)"
50
+ FAILED=$((FAILED + 1))
51
+ fi
52
+ else
53
+ # Expected error
54
+ if echo "$content" | grep -qE "error|missing|required|invalid|must|Authentication"; then
55
+ echo "✅ $test_name"
56
+ PASSED=$((PASSED + 1))
57
+ else
58
+ echo "❌ $test_name (expected error, got success or no error)"
59
+ FAILED=$((FAILED + 1))
60
+ fi
61
+ fi
62
+ }
63
+
64
+ # Test 1: Authentication check - should fail without auth
65
+ echo "Test 1: Deployment without authentication"
66
+ TEST_PROJECT="dollie-test-$(date +%s)"
67
+ run_test "Deployment without auth" "$TEST_PROJECT" "Test deployment without authentication" "https://github.com/vercel/next.js.git" "canary" "3000" "error"
68
+
69
+ # Test 2: Authenticate first
70
+ echo "Test 2: Authenticate with test account"
71
+ LOGIN_REQ='{"jsonrpc":"2.0","method":"tools/call","params":{"name":"mlgym_auth_login","arguments":{"email":"test-dollie@ezb.net","password":"TestPassword123!"}},"id":100}'
72
+ LOGIN_RESP=$(echo "$LOGIN_REQ" | node index.js 2>/dev/null | tail -1)
73
+ LOGIN_STATUS=$(echo "$LOGIN_RESP" | jq -r '.result.content[0].text' | jq -r '.status' 2>/dev/null)
74
+
75
+ if [ "$LOGIN_STATUS" = "authenticated" ]; then
76
+ echo "✅ Test 2: Authentication successful"
77
+ PASSED=$((PASSED + 1))
78
+ else
79
+ # Try to create the user first
80
+ echo "Creating test user..."
81
+ CREATE_REQ='{"jsonrpc":"2.0","method":"tools/call","params":{"name":"mlgym_user_create","arguments":{"email":"test-dollie@ezb.net","name":"Dollie Test User","password":"TestPassword123!","accept_terms":true}},"id":101}'
82
+ CREATE_RESP=$(echo "$CREATE_REQ" | node index.js 2>/dev/null | tail -1)
83
+
84
+ # Try login again
85
+ LOGIN_RESP=$(echo "$LOGIN_REQ" | node index.js 2>/dev/null | tail -1)
86
+ LOGIN_STATUS=$(echo "$LOGIN_RESP" | jq -r '.result.content[0].text' | jq -r '.status' 2>/dev/null)
87
+
88
+ if [ "$LOGIN_STATUS" = "authenticated" ]; then
89
+ echo "✅ Test 2: Authentication successful (after user creation)"
90
+ PASSED=$((PASSED + 1))
91
+ else
92
+ echo "⚠️ Test 2: Could not authenticate - skipping authenticated tests"
93
+ FAILED=$((FAILED + 1))
94
+ fi
95
+ fi
96
+
97
+ # Test 3: Valid deployment with public repo
98
+ echo "Test 3: Valid deployment with public GitHub repo"
99
+ TEST_PROJECT="dollie-valid-$(date +%s)"
100
+ run_test "Valid public repo deployment" "$TEST_PROJECT" "Test Next.js deployment from public repo" "https://github.com/vercel/next-learn.git" "main" "3000" "success"
101
+
102
+ # Test 4: Missing project_name
103
+ run_test "Missing project_name" "" "Test deployment" "https://github.com/vercel/next.js.git" "" "" "error"
104
+
105
+ # Test 5: Missing project_description
106
+ run_test "Missing description" "test-project" "" "https://github.com/vercel/next.js.git" "" "" "error"
107
+
108
+ # Test 6: Missing git_repo_url
109
+ run_test "Missing git repo URL" "test-project" "Test deployment" "" "" "" "error"
110
+
111
+ # Test 7: Invalid project_name format (uppercase)
112
+ run_test "Invalid project name (uppercase)" "INVALID-NAME" "Test deployment" "https://github.com/vercel/next.js.git" "" "" "error"
113
+
114
+ # Test 8: Invalid project_name format (spaces)
115
+ run_test "Invalid project name (spaces)" "invalid name" "Test deployment" "https://github.com/vercel/next.js.git" "" "" "error"
116
+
117
+ # Test 9: Short project_name (< 3 chars)
118
+ run_test "Short project name" "ab" "Test deployment" "https://github.com/vercel/next.js.git" "" "" "error"
119
+
120
+ # Test 10: Short project_description (< 10 chars)
121
+ run_test "Short description" "test-project" "Short" "https://github.com/vercel/next.js.git" "" "" "error"
122
+
123
+ # Test 11: Invalid git URL format (not https)
124
+ run_test "Invalid git URL (not https)" "test-project" "Test deployment" "git@github.com:vercel/next.js.git" "" "" "error"
125
+
126
+ # Test 12: Invalid git URL format (no .git)
127
+ run_test "Invalid git URL (no .git)" "test-project" "Test deployment" "https://github.com/vercel/next.js" "" "" "error"
128
+
129
+ # Test 13: Custom app_port
130
+ echo "Test 13: Custom app_port (8080)"
131
+ TEST_PROJECT="dollie-port-$(date +%s)"
132
+ run_test "Custom app port 8080" "$TEST_PROJECT" "Test deployment with custom port" "https://github.com/vercel/next-learn.git" "main" "8080" "success"
133
+
134
+ # Test 14: Custom git_branch
135
+ echo "Test 14: Custom git_branch"
136
+ TEST_PROJECT="dollie-branch-$(date +%s)"
137
+ run_test "Custom git branch" "$TEST_PROJECT" "Test deployment with custom branch" "https://github.com/vercel/next-learn.git" "canary" "3000" "success"
138
+
139
+ # Test 15: SQL injection in project_name
140
+ run_test "SQL injection (project_name)" "'; DROP TABLE applications;--" "Test deployment" "https://github.com/vercel/next.js.git" "" "" "error"
141
+
142
+ # Test 16: SQL injection in git_repo_url
143
+ run_test "SQL injection (git_repo_url)" "test-project" "Test deployment" "https://github.com/vercel/next.js.git'; DROP TABLE--" "" "" "error"
144
+
145
+ # Test 17: XSS attempt in project_description
146
+ run_test "XSS attempt (description)" "test-project" "<script>alert('XSS')</script>" "https://github.com/vercel/next.js.git" "" "" "success"
147
+
148
+ # Test 18: Buffer overflow in project_name
149
+ echo "Test 18: Buffer overflow (very long project_name)"
150
+ LONG_NAME=$(python3 -c "print('a' * 1000)")
151
+ run_test "Buffer overflow (project_name)" "$LONG_NAME" "Test deployment" "https://github.com/vercel/next.js.git" "" "" "error"
152
+
153
+ # Test 19: Response format validation
154
+ echo "Test 19: Response format validation"
155
+ TEST_PROJECT="dollie-format-$(date +%s)"
156
+ REQ="{\"jsonrpc\":\"2.0\",\"method\":\"tools/call\",\"params\":{\"name\":\"deploy_dollie\",\"arguments\":{\"project_name\":\"$TEST_PROJECT\",\"project_description\":\"Test deployment for response format validation\",\"git_repo_url\":\"https://github.com/vercel/next-learn.git\",\"git_branch\":\"main\"}},\"id\":102}"
157
+ RESP=$(echo "$REQ" | node index.js 2>/dev/null | tail -1)
158
+ CONTENT=$(echo "$RESP" | jq -r '.result.content[0].text')
159
+
160
+ HAS_SUCCESS=$(echo "$CONTENT" | jq -e '.success' > /dev/null 2>&1 && echo "yes" || echo "no")
161
+ HAS_URL=$(echo "$CONTENT" | jq -e '.deployment.url' > /dev/null 2>&1 && echo "yes" || echo "no")
162
+ HAS_SSH=$(echo "$CONTENT" | jq -e '.deployment.ssh' > /dev/null 2>&1 && echo "yes" || echo "no")
163
+ HAS_SSH_HOST=$(echo "$CONTENT" | jq -e '.deployment.ssh.host' > /dev/null 2>&1 && echo "yes" || echo "no")
164
+ HAS_SSH_PORT=$(echo "$CONTENT" | jq -e '.deployment.ssh.port' > /dev/null 2>&1 && echo "yes" || echo "no")
165
+ HAS_SSH_USERNAME=$(echo "$CONTENT" | jq -e '.deployment.ssh.username' > /dev/null 2>&1 && echo "yes" || echo "no")
166
+ HAS_SSH_PASSWORD=$(echo "$CONTENT" | jq -e '.deployment.ssh.password' > /dev/null 2>&1 && echo "yes" || echo "no")
167
+
168
+ if [ "$HAS_SUCCESS" = "yes" ] && [ "$HAS_URL" = "yes" ] && [ "$HAS_SSH" = "yes" ] && [ "$HAS_SSH_HOST" = "yes" ] && [ "$HAS_SSH_PORT" = "yes" ] && [ "$HAS_SSH_USERNAME" = "yes" ] && [ "$HAS_SSH_PASSWORD" = "yes" ]; then
169
+ echo "✅ Test 19: Response format valid"
170
+ PASSED=$((PASSED + 1))
171
+ else
172
+ echo "❌ Test 19: Response format invalid (success:$HAS_SUCCESS, url:$HAS_URL, ssh:$HAS_SSH, host:$HAS_SSH_HOST, port:$HAS_SSH_PORT, user:$HAS_SSH_USERNAME, pass:$HAS_SSH_PASSWORD)"
173
+ FAILED=$((FAILED + 1))
174
+ fi
175
+
176
+ # Test 20: SSH password strength validation
177
+ echo "Test 20: SSH password strength validation"
178
+ SSH_PASSWORD=$(echo "$CONTENT" | jq -r '.deployment.ssh.password')
179
+ PASSWORD_LENGTH=${#SSH_PASSWORD}
180
+ if [ $PASSWORD_LENGTH -ge 16 ]; then
181
+ echo "✅ Test 20: SSH password is strong (length: $PASSWORD_LENGTH)"
182
+ PASSED=$((PASSED + 1))
183
+ else
184
+ echo "❌ Test 20: SSH password is weak (length: $PASSWORD_LENGTH, expected >= 16)"
185
+ FAILED=$((FAILED + 1))
186
+ fi
187
+
188
+ echo ""
189
+ echo "==============================================="
190
+ echo "RESULTS: $PASSED passed, $FAILED failed (Total: $((PASSED + FAILED)))"
191
+ echo "==============================================="
192
+
193
+ if [ $FAILED -eq 0 ]; then
194
+ echo "✅ ALL TESTS PASSED"
195
+ exit 0
196
+ else
197
+ echo "⚠️ SOME TESTS FAILED"
198
+ exit 1
199
+ fi