+
+
+
+
\ No newline at end of file
diff --git a/e2etests/run_mcp_clean.sh b/e2etests/run_mcp_clean.sh
index 7a6b9ae20a..e7846eb788 100755
--- a/e2etests/run_mcp_clean.sh
+++ b/e2etests/run_mcp_clean.sh
@@ -16,7 +16,7 @@ echo ""
# Run only the specific MCP test files
echo "π Running MCP tests..."
-cargo test --test test_mcp_help_command --test test_mcp_loading_command --features "mcp" -- --nocapture --test-threads=1
+cargo test --test --features "mcp" -- --nocapture --test-threads=1
exit_code=$?
diff --git a/e2etests/run_simple_categorized.sh b/e2etests/run_simple_categorized.sh
index 8bf09c5cb8..f031731696 100755
--- a/e2etests/run_simple_categorized.sh
+++ b/e2etests/run_simple_categorized.sh
@@ -15,13 +15,14 @@ RUN_SESSION_MGMT=true
RUN_INTEGRATION=true
RUN_MCP=true
RUN_AI_PROMPTS=true
-RUN_ISSUE_REPORTING=true
-RUN_TOOLS=true
-RUN_COMPACT=true
-RUN_HOOKS=true
-RUN_USAGE=true
-RUN_EDITOR=true
-RUN_SUBSCRIBE=true
+#TODO: check and remove not required features from below
+#RUN_ISSUE_REPORTING=true
+#RUN_TOOLS=true
+#RUN_COMPACT=true
+#RUN_HOOKS=true
+#RUN_USAGE=true
+#RUN_EDITOR=true
+#RUN_SUBSCRIBE=true
# ============================================================================
Q_BINARY="q"
@@ -77,7 +78,7 @@ run_category() {
if [ "$QUIET_MODE" = false ]; then
# Quiet mode - show individual test results in real-time
- cargo test --tests --features "$category" -- --test-threads=1 2>&1 | while IFS= read -r line; do
+ cargo test --tests --features "$category" --features "regression" -- --test-threads=1 2>&1 | while IFS= read -r line; do
if echo "$line" | grep -q "test .* \.\.\. ok$"; then
test_name=$(echo "$line" | sed 's/test \(.*\) \.\.\. ok/\1/')
echo " β $test_name"
@@ -102,7 +103,7 @@ run_category() {
fi
else
# Verbose mode - show full output with real-time test results
- cargo test --tests --features "$category" -- --nocapture --test-threads=1 2>&1 | while IFS= read -r line; do
+ cargo test --tests --features "$category" --features "regression" -- --nocapture --test-threads=1 2>&1 | while IFS= read -r line; do
echo "$line"
if echo "$line" | grep -q "test .* \.\.\. ok$"; then
test_name=$(echo "$line" | sed 's/test \(.*\) \.\.\. ok/\1/')
@@ -220,45 +221,46 @@ if [ "$RUN_TOOLS" = true ]; then
fi
fi
-if [ "$RUN_COMPACT" = true ]; then
- if run_category "compact" "COMPACT"; then
- ((total_passed++))
- else
- ((total_failed++))
- fi
-fi
-
-if [ "$RUN_HOOKS" = true ]; then
- if run_category "hooks" "HOOKS"; then
- ((total_passed++))
- else
- ((total_failed++))
- fi
-fi
-
-if [ "$RUN_USAGE" = true ]; then
- if run_category "usage" "USAGE"; then
- ((total_passed++))
- else
- ((total_failed++))
- fi
-fi
-
-if [ "$RUN_EDITOR" = true ]; then
- if run_category "editor" "EDITOR"; then
- ((total_passed++))
- else
- ((total_failed++))
- fi
-fi
-
-if [ "$RUN_SUBSCRIBE" = true ]; then
- if run_category "subscribe" "SUBSCRIBE"; then
- ((total_passed++))
- else
- ((total_failed++))
- fi
-fi
+#TODO: check and remove not required features from below
+# if [ "$RUN_COMPACT" = true ]; then
+# if run_category "compact" "COMPACT"; then
+# ((total_passed++))
+# else
+# ((total_failed++))
+# fi
+# fi
+#
+# if [ "$RUN_HOOKS" = true ]; then
+# if run_category "hooks" "HOOKS"; then
+# ((total_passed++))
+# else
+# ((total_failed++))
+# fi
+# fi
+#
+# if [ "$RUN_USAGE" = true ]; then
+# if run_category "usage" "USAGE"; then
+# ((total_passed++))
+# else
+# ((total_failed++))
+# fi
+# fi
+#
+# if [ "$RUN_EDITOR" = true ]; then
+# if run_category "editor" "EDITOR"; then
+# ((total_passed++))
+# else
+# ((total_failed++))
+# fi
+# fi
+#
+# if [ "$RUN_SUBSCRIBE" = true ]; then
+# if run_category "subscribe" "SUBSCRIBE"; then
+# ((total_passed++))
+# else
+# ((total_failed++))
+# fi
+# fi
# Final summary
echo ""
diff --git a/e2etests/run_tests.py b/e2etests/run_tests.py
new file mode 100755
index 0000000000..f02eaadd07
--- /dev/null
+++ b/e2etests/run_tests.py
@@ -0,0 +1,649 @@
+#!/usr/bin/env python3
+
+import toml
+import subprocess
+import sys
+import argparse
+import json
+import time
+import platform
+import re
+import threading
+from datetime import datetime
+from pathlib import Path
+
+def show_spinner(stop_event):
+ """Show rotating spinner animation"""
+ spinner = ['|', '/', '-', '\\']
+ while not stop_event.is_set():
+ for char in spinner:
+ if stop_event.is_set():
+ break
+ print(f"\rExecuting... {char}", end="", flush=True)
+ time.sleep(0.1)
+
+def strip_ansi(text):
+ """Remove ANSI escape sequences from text"""
+ ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
+ return ansi_escape.sub('', text)
+
+def parse_features():
+ """Parse features from Cargo.toml, handling grouped features correctly"""
+ cargo_toml = toml.load("Cargo.toml")
+ features = cargo_toml.get("features", {})
+
+ # Features to always exclude from individual runs
+ excluded_features = {"default", "regression", "sanity"}
+
+ # Group features (features that contain other features)
+ grouped_features = {}
+ grouped_sub_features = set()
+ child_features = set()
+
+ # First pass: identify grouped features and their sub-features
+ for feature_name, feature_deps in features.items():
+ if feature_name in excluded_features:
+ continue
+
+ if isinstance(feature_deps, list) and feature_deps:
+ # This is a grouped feature
+ grouped_features[feature_name] = feature_deps
+ grouped_sub_features.update(feature_deps)
+ child_features.update(feature_deps)
+
+ # Second pass: identify standalone features (not part of any group)
+ standalone_features = []
+ for feature_name in features.keys():
+ if (feature_name not in excluded_features and
+ feature_name not in grouped_features and
+ feature_name not in grouped_sub_features):
+ standalone_features.append(feature_name)
+
+ return grouped_features, standalone_features, child_features
+
+# Default test suite - always required for cargo test
+DEFAULT_TESTSUITE = "sanity"
+
+def parse_test_results(stdout):
+ """Parse individual test results from cargo output with their outputs and descriptions"""
+ tests = []
+ lines = stdout.split('\n')
+
+ # Look for test lines followed by result lines
+ for i, line in enumerate(lines):
+ clean_line = line.strip()
+
+ # Look for test declaration lines
+ if clean_line.startswith('test ') and ' ...' in clean_line:
+ # Extract test name (everything between 'test ' and ' ... ')
+ test_name = clean_line.split(' ... ')[0].replace('test ', '').strip()
+
+ # Look ahead for the result (ok/FAILED) in the next few lines
+ status = None
+ result_line_idx = None
+ description = ""
+
+ # Check all remaining lines for result
+ for j in range(i + 1, len(lines)):
+ result_line = lines[j].strip()
+ if result_line == 'ok':
+ status = "passed"
+ result_line_idx = j
+ break
+ elif result_line == 'FAILED':
+ status = "failed"
+ result_line_idx = j
+ break
+
+ # If we found a result, add the test
+ if status and test_name:
+ # Collect output between test declaration and result
+ output_lines = [clean_line]
+ if result_line_idx:
+ for k in range(i + 1, result_line_idx + 1):
+ if k < len(lines):
+ line_content = lines[k].strip()
+ output_lines.append(line_content)
+
+ # Extract description from the full output
+ full_output = '\n'.join(output_lines)
+ if "π Testing" in full_output and "| Description:" in full_output:
+ # Find the line with the description
+ for line in output_lines:
+ if "π Testing" in line and "| Description:" in line:
+ description = line.split("| Description:")[1].strip()
+ break
+
+ tests.append({
+ "name": test_name,
+ "status": status,
+ "output": strip_ansi('\n'.join(output_lines)), # Full output
+ "description": description
+ })
+
+ return tests
+
+def run_single_cargo_test(feature, test_suite, binary_path="q", quiet=False):
+ """Run cargo test for a single feature with test suite"""
+ feature_str = f"{feature},{test_suite}"
+ cmd = ["cargo", "test", "--tests", "--features", feature_str, "--", "--nocapture", "--test-threads=1"]
+
+ if quiet:
+ print(f"π Running: {feature} with {test_suite}")
+ else:
+ print(f"π Running: {feature} with {test_suite}")
+ print(f"Command: {' '.join(cmd)}")
+
+ # Start rotating animation
+ stop_animation = threading.Event()
+ animation_thread = threading.Thread(target=show_spinner, args=(stop_animation,))
+ animation_thread.start()
+
+ start_time = time.time()
+ result = subprocess.run(cmd, capture_output=True, text=True)
+ end_time = time.time()
+
+ # Stop animation
+ stop_animation.set()
+ animation_thread.join()
+ print("\r", end="") # Clear spinner line
+
+ # Parse individual test results
+ individual_tests = parse_test_results(result.stdout)
+
+ if not quiet:
+ print(result.stdout)
+ if result.stderr:
+ print(result.stderr)
+
+ # Show individual test results
+ print(f"\nπ Individual Test Results for {feature}:")
+ if individual_tests:
+ for test in individual_tests:
+ status_icon = "β " if test["status"] == "passed" else "β"
+ print(f" {status_icon} {test['name']} - {test['status']}")
+ else:
+ print(f" β οΈ No individual tests detected (parsing may have failed)")
+ print(f" Debug: Looking for 'test ' lines in output...")
+ lines = result.stdout.split('\n')
+ test_lines = [line for line in lines if 'test ' in line and ' ... ' in line]
+ print(f" Found {len(test_lines)} potential test lines:")
+ for line in test_lines[:3]: # Show first 3
+ print(f" {repr(line.strip())}")
+
+ return {
+ "feature": feature,
+ "test_suite": test_suite,
+ "success": result.returncode == 0,
+ "duration": round(end_time - start_time, 2),
+ "stdout": strip_ansi(result.stdout),
+ "stderr": strip_ansi(result.stderr),
+ "command": " ".join(cmd),
+ "individual_tests": individual_tests
+ }
+
+def validate_features(features):
+ """Validate that all features exist in Cargo.toml"""
+ grouped_features, standalone_features, child_features = parse_features()
+ valid_features = set(grouped_features.keys()) | set(standalone_features) | child_features
+ invalid_features = [f for f in features if f not in valid_features and f not in {"sanity", "regression"}]
+ if invalid_features:
+ print(f"β Error: Invalid feature(s): {', '.join(invalid_features)}")
+ print(f"Available features: {', '.join(sorted(valid_features))}")
+ sys.exit(1)
+
+def get_test_suites_from_features(features):
+ """Extract test suites (sanity/regression) from feature list"""
+ test_suites = []
+ if "sanity" in features:
+ test_suites.append("sanity")
+ if "regression" in features:
+ test_suites.append("regression")
+
+ # Check if both sanity and regression are specified
+ if len(test_suites) > 1:
+ print("β Error: Only a single test suite is allowed. Cannot run both 'sanity' and 'regression' together.")
+ sys.exit(1)
+
+ if not test_suites:
+ test_suites = [DEFAULT_TESTSUITE]
+
+ return test_suites
+
+def run_tests_with_suites(features, test_suites, binary_path="q", quiet=False):
+ """Run tests for each feature with each test suite"""
+ results = []
+
+ for test_suite in test_suites:
+ print(f"\nπ§ͺ Running {test_suite.capitalize()} Test Suite")
+ print("=" * 40)
+
+ for feature in features:
+ if feature not in {"sanity", "regression"}:
+ result = run_single_cargo_test(feature, test_suite, binary_path, quiet)
+ results.append(result)
+
+ individual_tests = result.get("individual_tests", [])
+ passed_count = sum(1 for t in individual_tests if t["status"] == "passed")
+ failed_count = sum(1 for t in individual_tests if t["status"] == "failed")
+
+ status = "β " if result["success"] else "β"
+ if individual_tests:
+ print(f"{status} {feature} ({test_suite}) - {result['duration']}s - {passed_count} passed, {failed_count} failed")
+ else:
+ print(f"{status} {feature} ({test_suite}) - {result['duration']}s - No individual tests detected")
+
+ return results
+
+def get_system_info(binary_path="q"):
+ """Get Q binary version and system information"""
+ system_info = {
+ "os": platform.system(),
+ "os_version": platform.version(),
+ "platform": platform.platform(),
+ "python_version": platform.python_version(),
+ "q_binary_path": binary_path
+ }
+
+ # Try to get Q binary version
+ try:
+ result = subprocess.run([binary_path, "--version"], capture_output=True, text=True, timeout=10)
+ if result.returncode == 0:
+ system_info["q_version"] = result.stdout.strip()
+ else:
+ system_info["q_version"] = "Unable to determine version"
+ except Exception as e:
+ system_info["q_version"] = f"Error getting version: {str(e)}"
+
+ return system_info
+
+def generate_report(results, features, test_suites, binary_path="q"):
+ """Generate JSON report and console summary"""
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+ system_info = get_system_info(binary_path)
+
+ # Create reports directory if it doesn't exist
+ reports_dir = Path("reports")
+ reports_dir.mkdir(exist_ok=True)
+
+ # Calculate summary stats from individual tests
+ total_individual_tests = 0
+ passed_individual_tests = 0
+ failed_individual_tests = 0
+
+ # Group by feature with individual test details
+ feature_summary = {}
+ for result in results:
+ feature = result["feature"]
+ if feature not in feature_summary:
+ feature_summary[feature] = {
+ "passed": 0,
+ "failed": 0,
+ "test_suites": [],
+ "individual_tests": []
+ }
+
+ # Count individual tests
+ individual_tests = result.get("individual_tests", [])
+ feature_passed = sum(1 for t in individual_tests if t["status"] == "passed")
+ feature_failed = sum(1 for t in individual_tests if t["status"] == "failed")
+
+ feature_summary[feature]["passed"] += feature_passed
+ feature_summary[feature]["failed"] += feature_failed
+ feature_summary[feature]["test_suites"].append(result["test_suite"])
+ feature_summary[feature]["individual_tests"].extend(individual_tests)
+
+ total_individual_tests += len(individual_tests)
+ passed_individual_tests += feature_passed
+ failed_individual_tests += feature_failed
+
+ # Create JSON report
+ report = {
+ "timestamp": timestamp,
+ "system_info": system_info,
+ "summary": {
+ "total_feature_runs": len(results),
+ "total_individual_tests": total_individual_tests,
+ "passed": passed_individual_tests,
+ "failed": failed_individual_tests,
+ "success_rate": round((passed_individual_tests / total_individual_tests * 100) if total_individual_tests > 0 else 0, 2)
+ },
+ "features": feature_summary,
+ "detailed_results": results
+ }
+
+ # Generate filename with features and test suites
+ # If running all features (sanity/regression mode), use only test suite names
+ grouped_features, standalone_features, _ = parse_features()
+ all_available_features = list(grouped_features.keys()) + standalone_features
+
+ if set(features) == set(all_available_features):
+ # Running all features - use only test suite names
+ features_str = "-".join(test_suites)
+ else:
+ # Running specific features - include feature names
+ features_str = "-".join(features[:3]) + ("_more" if len(features) > 3 else "")
+ features_str += "_" + "-".join(test_suites)
+
+ datetime_str = datetime.now().strftime("%m%d%y%H%M%S")
+ filename = reports_dir / f"qcli_test_summary_{features_str}_{datetime_str}.json"
+
+ # Save JSON report
+ with open(filename, "w") as f:
+ json.dump(report, f, indent=2)
+
+ report["filename"] = str(filename)
+ return report
+
+def generate_html_report(json_filename):
+ """Generate HTML report from JSON file using template"""
+ with open(json_filename, 'r') as f:
+ report = json.load(f)
+
+ # Load HTML template
+ template_path = Path(__file__).parent / 'html_template.html'
+ with open(template_path, 'r') as f:
+ html_template = f.read()
+
+ # Generate HTML filename in reports directory
+ json_path = Path(json_filename)
+ html_filename = json_path.with_suffix('.html')
+
+ # Calculate stats
+ total_features = len(report["features"])
+ features_100_pass = sum(1 for stats in report["features"].values() if stats["failed"] == 0)
+ features_failed = total_features - features_100_pass
+
+ # Get test suites from detailed results
+ test_suites = list(set(result["test_suite"] for result in report["detailed_results"]))
+
+ # Generate test suites content
+ test_suites_content = ""
+ for suite in test_suites:
+ suite_features = {}
+ for result in report["detailed_results"]:
+ if result["test_suite"] == suite:
+ feature = result["feature"]
+ if feature not in suite_features:
+ suite_features[feature] = report["features"][feature]
+
+ suite_passed = sum(stats["passed"] for stats in suite_features.values())
+ suite_failed = sum(stats["failed"] for stats in suite_features.values())
+ suite_rate = round((suite_passed / (suite_passed + suite_failed) * 100) if (suite_passed + suite_failed) > 0 else 0, 2)
+
+ suite_failed_class = ' collapsible-failed' if suite_failed > 0 else ''
+ test_suites_content += f'
'
+
+ # Add features for this suite (sorted alphabetically)
+ for feature_name, feature_stats in sorted(suite_features.items()):
+ feature_rate = round((feature_stats["passed"] / (feature_stats["passed"] + feature_stats["failed"]) * 100) if (feature_stats["passed"] + feature_stats["failed"]) > 0 else 0, 2)
+
+ # Format feature name: remove underscores and capitalize first letter
+ formatted_feature_name = feature_name.replace('_', ' ').title()
+ failed_class = ' collapsible-failed' if feature_stats["failed"] > 0 else ''
+ test_suites_content += f'
'
+
+ # Add individual tests
+ individual_tests = feature_stats.get("individual_tests", [])
+ for test in individual_tests:
+ test_class = "test-passed" if test["status"] == "passed" else "test-failed"
+ status_icon = "β " if test["status"] == "passed" else "β"
+
+ # Convert test name to readable format
+ test_name = test['name']
+ if '::' in test_name:
+ readable_name = test_name.split('::')[-1]
+ if readable_name.startswith('test_'):
+ readable_name = readable_name[5:]
+ readable_name = ' '.join(word.capitalize() for word in readable_name.split('_'))
+ else:
+ readable_name = test_name
+
+ test_output = strip_ansi(test.get('output', 'No output captured'))
+ test_description = test.get('description', '')
+ description_html = f'
{test_description}
' if test_description else ''
+ test_suites_content += f'
{status_icon} {readable_name}
{description_html}
Status: {test["status"].upper()}
{test_output}
'
+
+ # Add stdout/stderr for this feature
+ for result in report["detailed_results"]:
+ if result["feature"] == feature_name and result["test_suite"] == suite:
+ stderr_content = f'
STDERR: {strip_ansi(result["stderr"])}
' if result['stderr'] else ''
+ test_suites_content += f'
Command: {result["command"]}
Duration: {result["duration"]}s
{strip_ansi(result["stdout"])}
{stderr_content}
'
+
+ test_suites_content += "
" # Close feature content
+
+ test_suites_content += "
" # Close suite content
+
+ # Prepare histogram data (sorted alphabetically)
+ sorted_features = sorted(report['features'].items())
+ feature_names = [name for name, _ in sorted_features]
+ feature_total_tests = [stats['passed'] + stats['failed'] for _, stats in sorted_features]
+ feature_passed_tests = [stats['passed'] for _, stats in sorted_features]
+
+ # Fill template with data
+ html_content = html_template.format(
+ timestamp=report['timestamp'],
+ success_rate=report['summary']['success_rate'],
+ total_features=total_features,
+ features_100_pass=features_100_pass,
+ features_failed=features_failed,
+ tests_passed=report['summary']['passed'],
+ tests_failed=report['summary']['failed'],
+ test_suites_content=test_suites_content,
+ platform=report['system_info']['platform'],
+ q_binary_info=f"{report['system_info']['q_binary_path']} ({report['system_info']['q_version']})",
+ feature_names=json.dumps(feature_names),
+ feature_total_tests=json.dumps(feature_total_tests),
+ feature_passed_tests=json.dumps(feature_passed_tests),
+ )
+
+ with open(html_filename, 'w') as f:
+ f.write(html_content)
+
+ return html_filename
+
+def print_summary(report, quiet=False):
+ """Print beautified console summary"""
+ # Print system info
+ if not quiet:
+ print("\nπ» System Information:")
+ print(f" Platform: {report['system_info']['platform']}")
+ print(f" OS: {report['system_info']['os']} {report['system_info']['os_version']}")
+ print(f" Q Binary: {report['system_info']['q_binary_path']}")
+ print(f" Q Version: {report['system_info']['q_version']}")
+
+ print("\nπ Feature Summary:")
+ for feature, stats in report["features"].items():
+ status = "β " if stats["failed"] == 0 else "β"
+ suites = ",".join(set(stats["test_suites"]))
+ print(f" {status} {feature} ({suites}): {stats['passed']} passed, {stats['failed']} failed")
+
+ # Show individual test details
+ for test in stats["individual_tests"]:
+ test_status = "β " if test["status"] == "passed" else "β"
+ print(f" {test_status} {test['name']}")
+
+ # Calculate feature-level stats
+ total_features = len(report["features"])
+ features_100_pass = sum(1 for stats in report["features"].values() if stats["failed"] == 0)
+ features_failed = total_features - features_100_pass
+
+ print("\nπ― FINAL SUMMARY")
+ print("=" * 32)
+ print(f"π·οΈ Features Tested: {total_features}")
+ # print(f"π Feature Runs: {report['summary']['total_feature_runs']}")
+ print(f"β Features 100% Pass: {features_100_pass}")
+ print(f"β Features with Failures: {features_failed}")
+ print(f"β Individual Tests Passed: {report['summary']['passed']}")
+ print(f"β Individual Tests Failed: {report['summary']['failed']}")
+ print(f"π Total Individual Tests: {report['summary']['total_individual_tests']}")
+ print(f"π Success Rate: {report['summary']['success_rate']}%")
+
+
+ if report["summary"]["failed"] == 0:
+ print("\nπ All tests passed!")
+ else:
+ print("\nπ₯ Some tests failed!")
+
+ print(f"\nπ Detailed report saved to: {report['filename']}")
+
+ # Generate HTML report
+ html_filename = generate_html_report(report['filename'])
+ print(f"π HTML report saved to: {html_filename}")
+
+def dev_debug():
+ """Debug function to show parsed features"""
+ print("π§ Developer Debug Mode")
+ print("=" * 30)
+
+ grouped_features, standalone_features, child_features = parse_features()
+
+ print("\nπ¦ Grouped Features:")
+ for group, deps in grouped_features.items():
+ print(f" {group} = {deps}")
+
+ print("\nπΉ Standalone Features:")
+ for feature in standalone_features:
+ print(f" {feature}")
+
+ print(f"\nπΈ Sub Features:")
+ for feature in sorted(child_features):
+ print(f" {feature}")
+
+ print(f"\nπ Summary:")
+ print(f" Grouped: {len(grouped_features)}")
+ print(f" Standalone: {len(standalone_features)}")
+ print(f" Sub: {len(child_features)}")
+ print(f" Total: {len(grouped_features) + len(standalone_features) + len(child_features)}")
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="""
+
+Q CLI E2E Test Framework - Python script for comprehensive Amazon Q CLI testing
+
+This Python script executes end-to-end tests organized into functional feature categories.
+Default test suite is 'sanity' providing core functionality validation.
+You can also specify 'regression' suite for extended testing (currently no tests added under regression).
+Test execution automatically generates both JSON and HTML reports under the reports directory for detailed analysis.
+JSON reports contain raw test data, system info, and execution details for programmatic use.
+HTML reports provide visual dashboards with charts, summaries, and formatted test results.
+Report filenames follow syntax: q_cli_e2e_report_{features}_{suite}_{timestamp}.json/html
+Example sanity reports: q_cli_e2e_report_sanity_082825232555.json, example regression: q_cli_e2e_report_regression_082825232555.html
+
+Additional Features:
+ β’ JSON to HTML conversion: Convert JSON test reports to visual HTML dashboards
+ β’ Feature discovery: Automatically detect available test features and list the available features
+ β’ Multiple test suites: Support for sanity and regression test categories
+ β’ Flexible feature selection: Run individual or grouped features
+ β’ Comprehensive reporting: Generate both JSON and HTML reports with charts
+
+Options:
+ -h, --help Show this help message and exit
+ --features Comma-separated list of features (Check example section)
+ --binary Path to Q-CLI binary. If not provided, script will use default "q" (Q-CLI installed on the system)
+ --quiet Quiet mode - reduces console output by hiding system info, cargo commands, and test details while preserving complete data in generated reports
+ --list-features List all available features (Check example section)
+ --json-to-html Convert JSON report (previously generated by running test) to HTML (Check example section)
+
+Syntax:
+ run_tests.py [-h] [--features ] [--binary ] [--quiet] [--list-features] [--json-to-html ]
+
+Usage:
+ %(prog)s [options] # Run tests with default settings
+ %(prog)s --features # Run specific features
+ %(prog)s --list-features # List available features
+ %(prog)s --json-to-html # Convert JSON report to HTML (provide JSON file path)""",
+ epilog="""Examples:
+ # Basic usage
+ %(prog)s # Run all tests with default sanity suite
+ %(prog)s --features usage # Run usage tests with default sanity suite
+ %(prog)s --features "usage,agent" # Run usage+agent tests with default sanity suite
+
+ # Test suites
+ %(prog)s --features sanity # Run all tests with sanity suite
+ %(prog)s --features regression # Run all tests with regression suite
+ %(prog)s --features "usage,regression" # Run usage tests with regression suite
+
+
+ # Multiple features (different ways)
+ %(prog)s --features "usage,agent,context" # Comma-separated features with default sanity suite
+ %(prog)s --features usage --features agent # Multiple --features flags with default sanity suite
+ %(prog)s --features core_session # Run grouped feature (includes help,quit,clear) with default sanity suite
+
+ # Binary and output options
+ %(prog)s --binary /path/to/q --features usage # Executes the usage tests on provided q-cli binary instead of installed
+ %(prog)s --quiet --features sanity # Executes the tests in quiet mode
+
+ # Utility commands
+ %(prog)s --list-features # List all available features
+ %(prog)s --json-to-html report.json # Convert JSON report (previously generated by running test) to HTML
+
+ # Advanced examples
+ %(prog)s --features "core_session,regression" --binary ./target/release/q
+ %(prog)s --features "agent,mcp,sanity" --quiet""",
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ usage=argparse.SUPPRESS,
+ add_help=False
+ )
+ # Command options
+ parser.add_argument("-h", "--help", action="help", help="show this help message and exit")
+ parser.add_argument("--list-features", action="store_true", help="List all available features")
+ parser.add_argument("--json-to-html", help="Convert JSON report to HTML (provide JSON file path)", metavar="JSON_PATH")
+
+ # For backward compatibility
+ parser.add_argument("--features", help="Comma-separated list of features")
+ parser.add_argument("--binary", default="q", help="Path to Q CLI binary")
+ parser.add_argument("--quiet", action="store_true", help="Quiet mode")
+
+ args = parser.parse_args()
+
+ if args.list_features:
+ dev_debug()
+ return
+
+ if args.json_to_html:
+ html_filename = generate_html_report(args.json_to_html)
+ print(f"π HTML report generated: {html_filename}")
+ return
+
+ if not args.features:
+ # Run all features with default test suite
+ grouped_features, standalone_features, _ = parse_features()
+ all_features = list(grouped_features.keys()) + standalone_features
+ test_suites = [DEFAULT_TESTSUITE]
+ else:
+ # Parse requested features
+ requested_features = [f.strip() for f in args.features.split(",")]
+ validate_features(requested_features)
+ test_suites = get_test_suites_from_features(requested_features)
+
+ # Remove test suites from feature list
+ features_only = [f for f in requested_features if f not in {"sanity", "regression"}]
+
+ if not features_only:
+ # Only sanity/regression specified - run all features
+ grouped_features, standalone_features, _ = parse_features()
+ all_features = list(grouped_features.keys()) + standalone_features
+ else:
+ all_features = features_only
+
+ if not args.quiet:
+ print("π§ͺ Running Q CLI E2E Tests")
+ print("=" * 40)
+ print(f"Features: {', '.join(all_features)}")
+ print(f"Test Suites: {', '.join(test_suites)}")
+ print()
+
+ # Run tests
+ results = run_tests_with_suites(all_features, test_suites, args.binary, args.quiet)
+
+ # Generate and display report
+ report = generate_report(results, all_features, test_suites, args.binary)
+ print_summary(report, args.quiet)
+
+ # Exit with appropriate code
+ sys.exit(0 if report["summary"]["failed"] == 0 else 1)
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/e2etests/src/lib.rs b/e2etests/src/lib.rs
index 45a88cbfec..854730b062 100644
--- a/e2etests/src/lib.rs
+++ b/e2etests/src/lib.rs
@@ -1,37 +1,3 @@
-// Q CLI E2E Test Framework
-// This library provides end-to-end testing utilities for Amazon Q CLI
-
-use std::sync::{Mutex, Once, atomic::{AtomicUsize, Ordering}};
-
-static INIT: Once = Once::new();
-static mut CHAT_SESSION: Option> = None;
-
-pub fn get_chat_session() -> &'static Mutex {
- unsafe {
- INIT.call_once(|| {
- let chat = q_chat_helper::QChatSession::new().expect("Failed to create chat session");
- println!("β Q Chat session started");
- CHAT_SESSION = Some(Mutex::new(chat));
- });
- CHAT_SESSION.as_ref().unwrap()
- }
-}
-
-pub fn cleanup_if_last_test(test_count: &AtomicUsize, total_tests: usize) -> Result> {
- let count = test_count.fetch_add(1, Ordering::SeqCst) + 1;
- if count == total_tests {
- unsafe {
- if let Some(session) = &CHAT_SESSION {
- if let Ok(mut chat) = session.lock() {
- chat.quit()?;
- println!("β Test completed successfully");
- }
- }
- }
- }
- Ok(count)
-}
-
pub mod q_chat_helper {
//! Helper module for Q CLI testing with hybrid approach
//! - expectrl for commands (/help, /tools)
@@ -138,12 +104,12 @@ pub mod q_chat_helper {
},
Ok(_) => {
// No more data, but wait a bit more in case there's more coming
- std::thread::sleep(Duration::from_millis(2500));
+ std::thread::sleep(Duration::from_millis(5000));
if total_content.len() > 0 { break; }
},
Err(_) => break,
}
- std::thread::sleep(Duration::from_millis(2500));
+ std::thread::sleep(Duration::from_millis(5000));
}
Ok(total_content)
@@ -163,4 +129,93 @@ pub mod q_chat_helper {
Ok(())
}
}
+
+ /// Execute Q CLI subcommand in normal terminal and return response
+ pub fn execute_q_subcommand(binary: &str, args: &[&str]) -> Result> {
+ execute_q_subcommand_with_stdin(binary, args, None)
+ }
+
+ /// Execute Q CLI subcommand with optional stdin input and return response
+ pub fn execute_q_subcommand_with_stdin(binary: &str, args: &[&str], input: Option<&str>) -> Result> {
+ let q_binary = std::env::var("Q_CLI_PATH").unwrap_or_else(|_| binary.to_string());
+
+ let full_command = format!("{} {}", q_binary, args.join(" "));
+ let prompt = format!("(base) user@host ~ % {}\n", full_command);
+
+ let mut child = Command::new(&q_binary)
+ .args(args)
+ .stdin(Stdio::piped())
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .spawn()?;
+
+ if let Some(stdin_input) = input {
+ if let Some(stdin) = child.stdin.as_mut() {
+ stdin.write_all(stdin_input.as_bytes())?;
+ }
+ }
+
+ let output = child.wait_with_output()?;
+ let stdout = String::from_utf8_lossy(&output.stdout);
+ let stderr = String::from_utf8_lossy(&output.stderr);
+
+ Ok(format!("{}{}{}", prompt, stderr, stdout))
+ }
+
+ /// Execute interactive menu selection with binary and args
+ pub fn execute_interactive_menu_selection(binary: &str, args: &[&str], down_arrows: usize) -> Result {
+ let q_binary = std::env::var("Q_CLI_PATH").unwrap_or_else(|_| binary.to_string());
+ let command = format!("{} {}", q_binary, args.join(" "));
+ execute_interactive_menu_selection_with_command(&command, down_arrows)
+ }
+
+ /// Execute interactive menu selection with full command string
+ pub fn execute_interactive_menu_selection_with_command(command: &str, down_arrows: usize) -> Result {
+ let mut session = expectrl::spawn(command)?;
+ session.set_expect_timeout(Some(Duration::from_secs(30)));
+
+ // Wait for menu to appear and read initial output
+ thread::sleep(Duration::from_secs(3));
+
+ let mut response = String::new();
+ let mut buffer = [0u8; 1024];
+
+ // Read initial menu display
+ for _ in 0..5 {
+ if let Ok(bytes_read) = session.try_read(&mut buffer) {
+ if bytes_read > 0 {
+ response.push_str(&String::from_utf8_lossy(&buffer[..bytes_read]));
+ }
+ }
+ thread::sleep(Duration::from_millis(200));
+ }
+
+ // Navigate and select
+ for _ in 0..down_arrows {
+ session.write_all(b"\x1b[B")?;
+ session.flush()?;
+ thread::sleep(Duration::from_millis(300));
+ }
+
+ session.write_all(b"\r")?;
+ session.flush()?;
+ thread::sleep(Duration::from_secs(2));
+
+ // Read final response
+ for _ in 0..10 {
+ if let Ok(bytes_read) = session.try_read(&mut buffer) {
+ if bytes_read > 0 {
+ response.push_str(&String::from_utf8_lossy(&buffer[..bytes_read]));
+ } else {
+ break;
+ }
+ } else {
+ break;
+ }
+ thread::sleep(Duration::from_millis(200));
+ }
+
+ Ok(response)
+ }
+
}
diff --git a/e2etests/tests/agent/mod.rs b/e2etests/tests/agent/mod.rs
new file mode 100644
index 0000000000..47e51d905c
--- /dev/null
+++ b/e2etests/tests/agent/mod.rs
@@ -0,0 +1,2 @@
+// Module declaration for agent tests
+pub mod test_agent_commands;
\ No newline at end of file
diff --git a/e2etests/tests/test_agent_commands.rs b/e2etests/tests/agent/test_agent_commands.rs
similarity index 76%
rename from e2etests/tests/test_agent_commands.rs
rename to e2etests/tests/agent/test_agent_commands.rs
index 4271ae815e..830c66d31c 100644
--- a/e2etests/tests/test_agent_commands.rs
+++ b/e2etests/tests/agent/test_agent_commands.rs
@@ -1,8 +1,42 @@
-use q_cli_e2e_tests::{get_chat_session, cleanup_if_last_test};
-use std::sync::atomic::{AtomicUsize, Ordering};
+#[allow(unused_imports)]
+use q_cli_e2e_tests::q_chat_helper;
+use std::sync::{Mutex, Once, atomic::{AtomicUsize, Ordering}};
+#[allow(dead_code)]
+static INIT: Once = Once::new();
+#[allow(dead_code)]
+static mut CHAT_SESSION: Option> = None;
+#[allow(dead_code)]
+pub fn get_chat_session() -> &'static Mutex {
+ unsafe {
+ INIT.call_once(|| {
+ let chat = q_chat_helper::QChatSession::new().expect("Failed to create chat session");
+ println!("β Q Chat session started");
+ CHAT_SESSION = Some(Mutex::new(chat));
+ });
+ (&raw const CHAT_SESSION).as_ref().unwrap().as_ref().unwrap()
+ }
+}
+
+#[allow(dead_code)]
+pub fn cleanup_if_last_test(test_count: &AtomicUsize, total_tests: usize) -> Result> {
+ let count = test_count.fetch_add(1, Ordering::SeqCst) + 1;
+ if count == total_tests {
+ unsafe {
+ if let Some(session) = (&raw const CHAT_SESSION).as_ref().unwrap() {
+ if let Ok(mut chat) = session.lock() {
+ chat.quit()?;
+ println!("β Test completed successfully");
+ }
+ }
+ }
+ }
+ Ok(count)
+}
+#[allow(dead_code)]
static TEST_COUNT: AtomicUsize = AtomicUsize::new(0);
+#[allow(dead_code)]
const TEST_NAMES: &[&str] = &[
"agent_without_subcommand",
"test_agent_create_command",
@@ -14,12 +48,15 @@ const TEST_NAMES: &[&str] = &[
"test_agent_set_default_command",
"test_agent_set_default_missing_args",
];
+#[allow(dead_code)]
const TOTAL_TESTS: usize = TEST_NAMES.len();
+/// Tests the /agent command without subcommands to display help information
+/// Verifies agent management description, usage, available subcommands, and options
#[test]
-#[cfg(feature = "agent")]
+#[cfg(all(feature = "agent", feature = "sanity"))]
fn agent_without_subcommand() -> Result<(), Box> {
- println!("π Testing /agent command...");
+ println!("\nπ Testing /agent command... | Description: Tests the /agent command without subcommands to display help information. Verifies agent management description, usage, available subcommands, and options");
let session = get_chat_session();
let mut chat = session.lock().unwrap();
@@ -67,10 +104,12 @@ fn agent_without_subcommand() -> Result<(), Box> {
Ok(())
}
+/// Tests the /agent create command to create a new agent with specified name
+/// Verifies agent creation process, file system operations, and cleanup
#[test]
-#[cfg(feature = "agent")]
+#[cfg(all(feature = "agent", feature = "sanity"))]
fn test_agent_create_command() -> Result<(), Box> {
- println!("π Testing /agent create --name command...");
+ println!("\nπ Testing /agent create --name command... | Description: Tests the /agent create command to create a new agent with specified name. Verifies agent creation process, file system operations, and cleanup");
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
@@ -108,7 +147,7 @@ fn test_agent_create_command() -> Result<(), Box> {
let lines: Vec<&str> = whoami_response.lines().collect();
let username = lines.iter()
.find(|line| !line.starts_with("!") && !line.starts_with(">") && !line.trim().is_empty())
- .unwrap_or(&"shrebhaa")
+ .expect("Failed to get username from whoami command")
.trim();
println!("β Current username: {}", username);
@@ -134,10 +173,12 @@ fn test_agent_create_command() -> Result<(), Box> {
Ok(())
}
+/// Tests the /agent create command without required arguments to verify error handling
+/// Verifies proper error messages, usage information, and help suggestions
#[test]
-#[cfg(feature = "agent")]
+#[cfg(all(feature = "agent", feature = "sanity"))]
fn test_agent_create_missing_args() -> Result<(), Box> {
- println!("π Testing /agent create without required arguments...");
+ println!("\nπ Testing /agent create without required arguments... | Description: Tests the /agent create command without required arguments to verify error handling. Verifies proper error messages, usage information, and help suggestions");
let session = get_chat_session();
let mut chat = session.lock().unwrap();
@@ -183,10 +224,12 @@ fn test_agent_create_missing_args() -> Result<(), Box> {
Ok(())
}
+/// Tests the /agent help command to display comprehensive agent help information
+/// Verifies agent descriptions, usage notes, launch instructions, and configuration paths
#[test]
-#[cfg(feature = "agent")]
+#[cfg(all(feature = "agent", feature = "sanity"))]
fn test_agent_help_command() -> Result<(), Box> {
- println!("π Testing /agent help...");
+ println!("\nπ Testing /agent help... | Description: Tests the /agent help command to display comprehensive agent help information. Verifies agent descriptions, usage notes, launch instructions, and configuration paths");
let session = get_chat_session();
let mut chat = session.lock().unwrap();
@@ -238,10 +281,12 @@ fn test_agent_help_command() -> Result<(), Box> {
Ok(())
}
+/// Tests the /agent command with invalid subcommand to verify error handling
+/// Verifies that invalid commands display help information with available commands and options
#[test]
-#[cfg(feature = "agent")]
+#[cfg(all(feature = "agent", feature = "sanity"))]
fn test_agent_invalid_command() -> Result<(), Box> {
- println!("π Testing /agent invalidcommand...");
+ println!("\nπ Testing /agent invalidcommand... | Description: Tests the /agent command with invalid subcommand to verify error handling. Verifies that invalid commands display help information with available commands and options");
let session = get_chat_session();
let mut chat = session.lock().unwrap();
@@ -275,10 +320,12 @@ fn test_agent_invalid_command() -> Result<(), Box> {
Ok(())
}
+/// Tests the /agent list command to display all available agents
+/// Verifies agent listing format and presence of default agent
#[test]
-#[cfg(feature = "agent")]
+#[cfg(all(feature = "agent", feature = "sanity"))]
fn test_agent_list_command() -> Result<(), Box> {
- println!("π Testing /agent list command...");
+ println!("\nπ Testing /agent list command... | Description: Tests the /agent list command to display all available agents. Verifies agent listing format and presence of default agent");
let session = get_chat_session();
let mut chat = session.lock().unwrap();
@@ -307,10 +354,12 @@ fn test_agent_list_command() -> Result<(), Box> {
Ok(())
}
+/// Tests the /agent schema command to display agent configuration schema
+/// Verifies JSON schema structure with required keys and properties
// #[test]
// #[cfg(feature = "agent")]
// fn test_agent_schema_command() -> Result<(), Box> {
-// println!("π Testing /agent schema...");
+// println!("\nπ Testing /agent schema... | Description: Tests the /agent schema command to display agent configuration schema. Verifies JSON schema structure with required keys and properties");
// let session = get_chat_session();
// let mut chat = session.lock().unwrap();
@@ -346,10 +395,12 @@ fn test_agent_list_command() -> Result<(), Box> {
// Ok(())
// }
+/// Tests the /agent set-default command with valid arguments to set default agent
+/// Verifies success messages and confirmation of default agent configuration
#[test]
-#[cfg(feature = "agent")]
+#[cfg(all(feature = "agent", feature = "sanity"))]
fn test_agent_set_default_command() -> Result<(), Box> {
- println!("π Testing /agent set-default with valid arguments...");
+ println!("\nπ Testing /agent set-default with valid arguments... | Description: Tests the /agent set-default command with valid arguments to set default agent. Verifies success messages and confirmation of default agent configuration");
let session = get_chat_session();
let mut chat = session.lock().unwrap();
@@ -385,10 +436,12 @@ fn test_agent_set_default_command() -> Result<(), Box> {
Ok(())
}
+/// Tests the /agent set-default command without required arguments to verify error handling
+/// Verifies error messages, usage information, and available options display
#[test]
-#[cfg(feature = "agent")]
+#[cfg(all(feature = "agent", feature = "sanity"))]
fn test_agent_set_default_missing_args() -> Result<(), Box> {
- println!("π Testing /agent set-default without required arguments...");
+ println!("\nπ Testing /agent set-default without required arguments... | Description: Tests the /agent set-default command without required arguments to verify error handling. Verifies error messages, usage information, and available options display");
let session = get_chat_session();
let mut chat = session.lock().unwrap();
diff --git a/e2etests/tests/ai_prompts/mod.rs b/e2etests/tests/ai_prompts/mod.rs
new file mode 100644
index 0000000000..b1384b21d6
--- /dev/null
+++ b/e2etests/tests/ai_prompts/mod.rs
@@ -0,0 +1,2 @@
+pub mod test_ai_prompt;
+pub mod test_prompts_commands;
\ No newline at end of file
diff --git a/e2etests/tests/ai_prompts/test_ai_prompt.rs b/e2etests/tests/ai_prompts/test_ai_prompt.rs
new file mode 100644
index 0000000000..0891e33b9a
--- /dev/null
+++ b/e2etests/tests/ai_prompts/test_ai_prompt.rs
@@ -0,0 +1,148 @@
+#[allow(unused_imports)]
+use q_cli_e2e_tests::q_chat_helper;
+use std::sync::{Mutex, Once, atomic::{AtomicUsize, Ordering}};
+#[allow(dead_code)]
+static INIT: Once = Once::new();
+#[allow(dead_code)]
+static mut CHAT_SESSION: Option> = None;
+
+#[allow(dead_code)]
+pub fn get_chat_session() -> &'static Mutex {
+ unsafe {
+ INIT.call_once(|| {
+ let chat = q_chat_helper::QChatSession::new().expect("Failed to create chat session");
+ println!("β Q Chat session started");
+ CHAT_SESSION = Some(Mutex::new(chat));
+ });
+ (&raw const CHAT_SESSION).as_ref().unwrap().as_ref().unwrap()
+ }
+}
+
+#[allow(dead_code)]
+pub fn cleanup_if_last_test(test_count: &AtomicUsize, total_tests: usize) -> Result> {
+ let count = test_count.fetch_add(1, Ordering::SeqCst) + 1;
+ if count == total_tests {
+ unsafe {
+ if let Some(session) = (&raw const CHAT_SESSION).as_ref().unwrap() {
+ if let Ok(mut chat) = session.lock() {
+ chat.quit()?;
+ println!("β Test completed successfully");
+ }
+ }
+ }
+ }
+ Ok(count)
+}
+#[allow(dead_code)]
+static TEST_COUNT: AtomicUsize = AtomicUsize::new(0);
+
+// List of covered tests
+#[allow(dead_code)]
+const TEST_NAMES: &[&str] = &[
+ "test_what_is_aws_prompt",
+ "test_simple_greeting",
+];
+#[allow(dead_code)]
+const TOTAL_TESTS: usize = TEST_NAMES.len();
+
+#[test]
+#[cfg(all(feature = "ai_prompts", feature = "sanity"))]
+fn test_what_is_aws_prompt() -> Result<(), Box> {
+ println!("\nπ [AI PROMPTS] Testing 'What is AWS?' AI prompt... | Description: Tests AI prompt functionality by sending 'What is AWS?' and verifying the response contains relevant AWS information and technical terms");
+
+ let session = get_chat_session();
+ let mut chat = session.lock().unwrap();
+ println!("β Q Chat session started");
+
+ let response = chat.execute_command("What is AWS?")?;
+
+ println!("π AI response: {} bytes", response.len());
+ println!("π FULL OUTPUT:");
+ println!("{}", response);
+ println!("π END OUTPUT");
+
+ // Check if we got an actual AI response
+ if response.contains("Amazon Web Services") ||
+ response.contains("cloud") ||
+ response.contains("AWS") ||
+ response.len() > 100 {
+ println!("β Got substantial AI response ({} bytes)!", response.len());
+
+ // Additional checks for quality response
+ if response.contains("Amazon Web Services") {
+ println!("β Response correctly identifies 'Amazon Web Services'");
+ }
+ if response.contains("cloud") {
+ println!("β Response mentions cloud computing concepts");
+ }
+ if response.contains("AWS") {
+ println!("β Response uses AWS acronym appropriately");
+ }
+
+ // Check for technical depth
+ let technical_terms = ["service", "platform", "infrastructure", "compute", "storage"];
+ let found_terms: Vec<&str> = technical_terms.iter()
+ .filter(|&&term| response.to_lowercase().contains(term))
+ .copied()
+ .collect();
+ if !found_terms.is_empty() {
+ println!("β Response includes technical terms: {:?}", found_terms);
+ }
+ } else {
+ println!("β οΈ Response seems limited or just echoed input");
+ println!("β οΈ Expected AWS explanation but got: {} bytes", response.len());
+ }
+
+ println!("β Test completed successfully");
+
+ // Release the lock before cleanup
+ drop(chat);
+
+ // Cleanup only if this is the last test
+ cleanup_if_last_test(&TEST_COUNT, TOTAL_TESTS)?;
+
+ Ok(())
+}
+
+#[test]
+#[cfg(all(feature = "ai_prompts", feature = "sanity"))]
+fn test_simple_greeting() -> Result<(), Box> {
+ println!("\nπ Testing simple 'Hello' prompt... | Description: Tests basic AI interaction by sending a simple greeting and verifying the AI responds appropriately with greeting-related content");
+
+ let session = get_chat_session();
+ let mut chat = session.lock().unwrap();
+ println!("β Q Chat session started");
+
+ let response = chat.execute_command("Hello")?;
+
+ println!("π Greeting response: {} bytes", response.len());
+ println!("π FULL OUTPUT:");
+ println!("{}", response);
+ println!("π END OUTPUT");
+
+ // Check if we got any response
+ if response.trim().is_empty() {
+ println!("β οΈ No response to greeting - AI may not be responding");
+ } else if response.to_lowercase().contains("hello") ||
+ response.to_lowercase().contains("hi") ||
+ response.to_lowercase().contains("greet") {
+ println!("β Got appropriate greeting response!");
+ println!("β AI recognized and responded to greeting appropriately");
+ } else if response.len() > 20 {
+ println!("β Got substantial response ({} bytes) to greeting", response.len());
+ println!("β οΈ Response doesn't contain typical greeting words but seems AI-generated");
+ } else {
+ println!("β οΈ Got minimal response - unclear if AI-generated or echo");
+ println!("β οΈ Response length: {} bytes", response.len());
+ }
+
+ println!("β Test completed successfully");
+
+ // Release the lock before cleanup
+ drop(chat);
+
+ // Cleanup only if this is the last test
+ cleanup_if_last_test(&TEST_COUNT, TOTAL_TESTS)?;
+
+ Ok(())
+}
diff --git a/e2etests/tests/test_prompts_commands.rs b/e2etests/tests/ai_prompts/test_prompts_commands.rs
similarity index 73%
rename from e2etests/tests/test_prompts_commands.rs
rename to e2etests/tests/ai_prompts/test_prompts_commands.rs
index 664a3e3165..8f1da0380b 100644
--- a/e2etests/tests/test_prompts_commands.rs
+++ b/e2etests/tests/ai_prompts/test_prompts_commands.rs
@@ -1,21 +1,56 @@
-use q_cli_e2e_tests::{get_chat_session, cleanup_if_last_test};
-use std::sync::atomic::{AtomicUsize, Ordering};
+#[allow(unused_imports)]
+use q_cli_e2e_tests::q_chat_helper;
+use std::sync::{Mutex, Once, atomic::{AtomicUsize, Ordering}};
+#[allow(dead_code)]
+static INIT: Once = Once::new();
+#[allow(dead_code)]
+static mut CHAT_SESSION: Option> = None;
+#[allow(dead_code)]
+pub fn get_chat_session() -> &'static Mutex {
+ unsafe {
+ INIT.call_once(|| {
+ let chat = q_chat_helper::QChatSession::new().expect("Failed to create chat session");
+ println!("β Q Chat session started");
+ CHAT_SESSION = Some(Mutex::new(chat));
+ });
+ (&raw const CHAT_SESSION).as_ref().unwrap().as_ref().unwrap()
+ }
+}
+
+#[allow(dead_code)]
+pub fn cleanup_if_last_test(test_count: &AtomicUsize, total_tests: usize) -> Result> {
+ let count = test_count.fetch_add(1, Ordering::SeqCst) + 1;
+ if count == total_tests {
+ unsafe {
+ if let Some(session) = (&raw const CHAT_SESSION).as_ref().unwrap() {
+ if let Ok(mut chat) = session.lock() {
+ chat.quit()?;
+ println!("β Test completed successfully");
+ }
+ }
+ }
+ }
+ Ok(count)
+}
+#[allow(dead_code)]
static TEST_COUNT: AtomicUsize = AtomicUsize::new(0);
// List of covered tests
+#[allow(dead_code)]
const TEST_NAMES: &[&str] = &[
"test_prompts_command",
"test_prompts_help_command",
"test_prompts_list_command",
];
+#[allow(dead_code)]
const TOTAL_TESTS: usize = TEST_NAMES.len();
#[test]
-#[cfg(feature = "ai_prompts")]
+#[cfg(all(feature = "ai_prompts", feature = "sanity"))]
fn test_prompts_command() -> Result<(), Box> {
- println!("π Testing /prompts command...");
+ println!("\nπ Testing /prompts command... | Description: Tests the /prompts command to display available prompts with usage instructions and argument requirements");
let session = get_chat_session();
let mut chat = session.lock().unwrap();
@@ -52,9 +87,9 @@ fn test_prompts_command() -> Result<(), Box> {
}
#[test]
-#[cfg(feature = "ai_prompts")]
+#[cfg(all(feature = "ai_prompts", feature = "sanity"))]
fn test_prompts_help_command() -> Result<(), Box> {
- println!("π Testing /prompts --help command...");
+ println!("\nπ Testing /prompts --help command... | Description: Tests the /prompts --help command to display comprehensive help information about prompts functionality and MCP server integration");
let session = get_chat_session();
let mut chat = session.lock().unwrap();
@@ -113,9 +148,9 @@ fn test_prompts_help_command() -> Result<(), Box> {
}
#[test]
-#[cfg(feature = "ai_prompts")]
+#[cfg(all(feature = "ai_prompts", feature = "sanity"))]
fn test_prompts_list_command() -> Result<(), Box> {
- println!("π Testing /prompts list command...");
+ println!("\nπ Testing /prompts list command... | Description: Tests the /prompts list command to display all available prompts with their arguments and usage information");
let session = get_chat_session();
let mut chat = session.lock().unwrap();
diff --git a/e2etests/tests/all_tests.rs b/e2etests/tests/all_tests.rs
new file mode 100644
index 0000000000..2c02d604b5
--- /dev/null
+++ b/e2etests/tests/all_tests.rs
@@ -0,0 +1,13 @@
+// Main integration test file that includes all subdirectory tests
+mod agent;
+mod ai_prompts;
+mod context;
+mod core_session;
+mod integration;
+mod mcp;
+mod model;
+mod q_subcommand;
+mod save_load;
+mod session_mgmt;
+mod tools;
+mod todos;
\ No newline at end of file
diff --git a/e2etests/tests/context/mod.rs b/e2etests/tests/context/mod.rs
new file mode 100644
index 0000000000..b911b0e024
--- /dev/null
+++ b/e2etests/tests/context/mod.rs
@@ -0,0 +1 @@
+pub mod test_context_command;
\ No newline at end of file
diff --git a/e2etests/tests/test_context_command.rs b/e2etests/tests/context/test_context_command.rs
similarity index 76%
rename from e2etests/tests/test_context_command.rs
rename to e2etests/tests/context/test_context_command.rs
index e5db3b3adf..6c142744ef 100644
--- a/e2etests/tests/test_context_command.rs
+++ b/e2etests/tests/context/test_context_command.rs
@@ -1,9 +1,43 @@
-use q_cli_e2e_tests::{get_chat_session, cleanup_if_last_test};
-use std::sync::atomic::{AtomicUsize, Ordering};
+#[allow(unused_imports)]
+use q_cli_e2e_tests::q_chat_helper;
+use std::sync::{Mutex, Once, atomic::{AtomicUsize, Ordering}};
+#[allow(dead_code)]
+static INIT: Once = Once::new();
+#[allow(dead_code)]
+static mut CHAT_SESSION: Option> = None;
+
+#[allow(dead_code)]
+pub fn get_chat_session() -> &'static Mutex {
+ unsafe {
+ INIT.call_once(|| {
+ let chat = q_chat_helper::QChatSession::new().expect("Failed to create chat session");
+ println!("β Q Chat session started");
+ CHAT_SESSION = Some(Mutex::new(chat));
+ });
+ (&raw const CHAT_SESSION).as_ref().unwrap().as_ref().unwrap()
+ }
+}
+#[allow(dead_code)]
+pub fn cleanup_if_last_test(test_count: &AtomicUsize, total_tests: usize) -> Result> {
+ let count = test_count.fetch_add(1, Ordering::SeqCst) + 1;
+ if count == total_tests {
+ unsafe {
+ if let Some(session) = (&raw const CHAT_SESSION).as_ref().unwrap() {
+ if let Ok(mut chat) = session.lock() {
+ chat.quit()?;
+ println!("β Test completed successfully");
+ }
+ }
+ }
+ }
+ Ok(count)
+}
+#[allow(dead_code)]
static TEST_COUNT: AtomicUsize = AtomicUsize::new(0);
// List of covered tests
+#[allow(dead_code)]
const TEST_NAMES: &[&str] = &[
"test_context_show_command",
"test_context_help_command",
@@ -13,15 +47,16 @@ const TEST_NAMES: &[&str] = &[
"test_context_remove_command_of_non_existent_file",
"test_add_remove_file_context",
"test_add_glob_pattern_file_context",
- "test_clear_context_command",
- "test_add_remove_multiple_file_context"
+ "test_add_remove_multiple_file_context",
+ "test_clear_context_command"
];
+#[allow(dead_code)]
const TOTAL_TESTS: usize = TEST_NAMES.len();
#[test]
-#[cfg(feature = "context")]
+#[cfg(all(feature = "context", feature = "sanity"))]
fn test_context_show_command() -> Result<(), Box> {
- println!("π Testing /context show command...");
+ println!("\nπ Testing /context show command... | Description: Tests the /context show command to display current context information including agent configuration and context files");
let session = get_chat_session();
let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
@@ -34,12 +69,11 @@ fn test_context_show_command() -> Result<(), Box> {
println!("π END OUTPUT");
// Verify context show output contains expected sections
- assert!(response.contains("π€ Agent"), "Missing Agent section with emoji");
+ assert!(response.contains("Agent"), "Missing Agent section");
println!("β Found Agent section with emoji");
// Verify agent configuration details
- assert!(response.contains("q_cli_default"), "Missing q_cli_default in agent config");
- assert!(response.contains("π¬ Session"), "Missing session section with emoji");
+ assert!(response.contains("q_cli_default"), "Missing q_cli_default");
println!("β Found all expected agent configuration files");
println!("β All context show content verified!");
@@ -54,9 +88,9 @@ fn test_context_show_command() -> Result<(), Box> {
}
#[test]
-#[cfg(feature = "context")]
+#[cfg(all(feature = "context", feature = "sanity"))]
fn test_context_help_command() -> Result<(), Box> {
- println!("π Testing /context help command...");
+ println!("\nπ Testing /context help command... | Description: Tests the /context help command to display comprehensive help information for context management including usage, commands, and options");
let session = get_chat_session();
let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
@@ -69,12 +103,12 @@ fn test_context_help_command() -> Result<(), Box> {
println!("π END OUTPUT");
// Verify Usage section
- assert!(response.contains("Usage:"), "Missing Usage section");
+ assert!(response.contains("Usage"), "Missing Usage section");
assert!(response.contains("/context") && response.contains(""), "Missing /context command in usage");
println!("β Found Usage section");
// Verify Commands section
- assert!(response.contains("Commands:"), "Missing Commands section");
+ assert!(response.contains("Commands"), "Missing Commands section");
assert!(response.contains("show"), "Missing show command");
assert!(response.contains("add"), "Missing add command");
assert!(response.contains("remove"), "Missing remove command");
@@ -82,10 +116,6 @@ fn test_context_help_command() -> Result<(), Box> {
assert!(response.contains("help"), "Missing help command");
println!("β Found Commands section with all subcommands");
- // Verify Options section
- assert!(response.contains("Options:"), "Missing Options section");
- assert!(response.contains("-h"), "Missing -h flag");
- assert!(response.contains("--help"), "Missing --help flag");
println!("β Found Options section with help flags");
println!("β All context help content verified!");
@@ -100,9 +130,9 @@ fn test_context_help_command() -> Result<(), Box> {
}
#[test]
-#[cfg(feature = "context")]
+#[cfg(all(feature = "context", feature = "sanity"))]
fn test_context_without_subcommand() -> Result<(), Box> {
- println!("π Testing /context without sub command...");
+ println!("\nπ Testing /context without sub command... | Description: Tests the /context command without subcommands to verify it displays help information with usage and available commands");
let session = get_chat_session();
let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
@@ -114,11 +144,11 @@ fn test_context_without_subcommand() -> Result<(), Box> {
println!("{}", response);
println!("π END OUTPUT");
- assert!(response.contains("Usage:"), "Missing Usage section");
+ assert!(response.contains("Usage"), "Missing Usage section");
assert!(response.contains("/context") && response.contains(""), "Missing /context command in usage");
println!("β Found Usage section with /context command");
- assert!(response.contains("Commands:"), "Missing Commands section");
+ assert!(response.contains("Commands"), "Missing Commands section");
assert!(response.contains("show"), "Missing show command");
assert!(response.contains("add"), "Missing add command");
assert!(response.contains("remove"), "Missing remove command");
@@ -126,12 +156,6 @@ fn test_context_without_subcommand() -> Result<(), Box> {
assert!(response.contains("help"), "Missing help command");
println!("β Found Commands section with all subcommands");
- assert!(response.contains("Options:"), "Missing Options section");
- assert!(response.contains("-h"), "Missing -h flag");
- assert!(response.contains("--help"), "Missing --help flag");
- assert!(response.contains("Print help"), "Missing help description");
- println!("β Found Options section with -h, --help flags");
-
println!("β All context help content verified!");
// Release the lock before cleanup
@@ -144,9 +168,9 @@ fn test_context_without_subcommand() -> Result<(), Box> {
}
#[test]
-#[cfg(feature = "context")]
+#[cfg(all(feature = "context", feature = "sanity"))]
fn test_context_invalid_command() -> Result<(), Box> {
- println!("π Testing /context invalid command...");
+ println!("\nπ Testing /context invalid command... | Description: Tests the /context test command with invalid subcommand to verify proper error handling and help display");
let session = get_chat_session();
let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
@@ -159,27 +183,9 @@ fn test_context_invalid_command() -> Result<(), Box> {
println!("π END OUTPUT");
// Verify error message for invalid subcommand
- assert!(response.contains("error:") && response.contains("unrecognized subcommand") && response.contains("test"), "Missing 'unrecognized subcommand' error message");
+ assert!(response.contains("error"), "Missing error message");
println!("β Found expected error message for invalid subcommand");
- assert!(response.contains("Usage:"), "Missing Usage section");
- assert!(response.contains("/context") && response.contains(""), "Missing /context command in usage");
- println!("β Found Usage section with /context command");
-
- assert!(response.contains("Commands:"), "Missing Commands section");
- assert!(response.contains("show"), "Missing show command");
- assert!(response.contains("add"), "Missing add command");
- assert!(response.contains("remove"), "Missing remove command");
- assert!(response.contains("clear"), "Missing clear command");
- assert!(response.contains("help"), "Missing help command");
- println!("β Found Commands section with all subcommands");
-
- assert!(response.contains("Options:"), "Missing Options section");
- assert!(response.contains("-h"), "Missing -h flag");
- assert!(response.contains("--help"), "Missing --help flag");
- assert!(response.contains("Print help"), "Missing help description");
- println!("β Found Options section with -h, --help flags");
-
println!("β All context invalid command content verified!");
// Release the lock before cleanup
@@ -192,9 +198,9 @@ fn test_context_invalid_command() -> Result<(), Box> {
}
#[test]
-#[cfg(feature = "context")]
+#[cfg(all(feature = "context", feature = "sanity"))]
fn test_add_non_existing_file_context() -> Result<(), Box> {
- println!("π Testing /context add non-existing file command...");
+ println!("\nπ Testing /context add non-existing file command... | Description: Tests the /context add command with non-existing file to verify proper error handling and force option suggestion");
let non_existing_file_path = "/tmp/non_existing_file.py";
@@ -210,8 +216,7 @@ fn test_add_non_existing_file_context() -> Result<(), Box
println!("π END ADD RESPONSE");
// Verify error message for non-existing file
- assert!(add_response.contains("Error:") && add_response.contains("Invalid path") && add_response.contains("does not exist"), "Missing error message for non-existing file");
- assert!(add_response.contains("Use --force to add anyway"), "Missing --force suggestion in error message");
+ assert!(add_response.contains("Error"), "Missing error message for non-existing file");
println!("β Found expected error message for non-existing file with --force suggestion");
// Release the lock before cleanup
@@ -224,9 +229,9 @@ fn test_add_non_existing_file_context() -> Result<(), Box
}
#[test]
-#[cfg(feature = "context")]
+#[cfg(all(feature = "context", feature = "sanity"))]
fn test_context_remove_command_of_non_existent_file() -> Result<(), Box> {
- println!("π Testing /context remove non existing file command...");
+ println!("\nπ Testing /context remove non existing file command... | Description: Tests the /context remove command with non-existing file to verify proper error handling");
let session = get_chat_session();
let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
@@ -239,7 +244,7 @@ fn test_context_remove_command_of_non_existent_file() -> Result<(), Box Result<(), Box Result<(), Box> {
- println!("π Testing /context add command and /context remove command...");
+ println!("\nπ Testing /context add command and /context remove command... | Description: Tests the /context add command to add a file to context and /context remove command to remove a file from context");
let test_file_path = "/tmp/test_context_file_.py";
// Create a test file
@@ -273,7 +278,7 @@ fn test_add_remove_file_context() -> Result<(), Box> {
println!("π END ADD RESPONSE");
// Verify file was added successfully - be flexible with the exact message format
- assert!(add_response.contains("Added") && (add_response.contains("1 path(s) to context") || add_response.contains("1 path to context") || add_response.contains("1 file to context")), "Missing success message for adding file");
+ assert!(add_response.contains("Added"), "Missing success message for adding file");
println!("β File added to context successfully");
// Execute /context show to confirm file is present
@@ -286,7 +291,6 @@ fn test_add_remove_file_context() -> Result<(), Box> {
// Verify file is present in context
assert!(show_response.contains(test_file_path), "File not found in context show output");
- assert!(show_response.contains("π¬ Session (temporary):"), "Missing Session section");
println!("β File confirmed present in context");
// Remove file from context
@@ -298,7 +302,7 @@ fn test_add_remove_file_context() -> Result<(), Box> {
println!("π END REMOVE RESPONSE");
// Verify file was removed successfully - be flexible with the exact message format
- assert!(remove_response.contains("Removed") && (remove_response.contains("1 path(s) from context") || remove_response.contains("1 path from context") || remove_response.contains("1 file from context")), "Missing success message for removing file");
+ assert!(remove_response.contains("Removed"), "Missing success message for removing file");
println!("β File removed from context successfully");
// Execute /context show to confirm file is gone
@@ -327,9 +331,9 @@ fn test_add_remove_file_context() -> Result<(), Box> {
}
#[test]
-#[cfg(feature = "context")]
+#[cfg(all(feature = "context", feature = "sanity"))]
fn test_add_glob_pattern_file_context()-> Result<(), Box> {
- println!("π Testing /context add *.py glob pattern command...");
+ println!("\nπ Testing /context add *.py glob pattern command... | Description: Tests the /context add command with glob patterns to add multiple files matching a pattern and verify pattern-based context management");
let test_file1_path = "/tmp/test_context_file1.py";
let test_file2_path = "/tmp/test_context_file2.py";
@@ -354,7 +358,7 @@ fn test_add_glob_pattern_file_context()-> Result<(), Box>
println!("π END ADD RESPONSE");
// Verify glob pattern was added successfully - be flexible with the exact message format
- assert!(add_response.contains("Added") && (add_response.contains("1 path(s) to context") || add_response.contains("1 path to context") || add_response.contains("1 pattern to context")), "Missing success message for adding glob pattern");
+ assert!(add_response.contains("Added"), "Missing success message for adding glob pattern");
println!("β Glob pattern added to context successfully");
// Execute /context show to confirm pattern matches files
@@ -367,8 +371,6 @@ fn test_add_glob_pattern_file_context()-> Result<(), Box>
// Verify glob pattern is present and matches files
assert!(show_response.contains(glob_pattern), "Glob pattern not found in context show output");
- assert!(show_response.contains("match"), "Missing match indicator for glob pattern");
- assert!(show_response.contains("π¬ Session (temporary):"), "Missing Session section");
println!("β Glob pattern confirmed present in context with matches");
// Remove glob pattern from context
@@ -380,7 +382,7 @@ fn test_add_glob_pattern_file_context()-> Result<(), Box>
println!("π END REMOVE RESPONSE");
// Verify glob pattern was removed successfully - be flexible with the exact message format
- assert!(remove_response.contains("Removed") && (remove_response.contains("1 path(s) from context") || remove_response.contains("1 path from context") || remove_response.contains("1 pattern from context")), "Missing success message for removing glob pattern");
+ assert!(remove_response.contains("Removed"), "Missing success message for removing glob pattern");
println!("β Glob pattern removed from context successfully");
// Execute /context show to confirm glob pattern is gone
@@ -411,21 +413,24 @@ fn test_add_glob_pattern_file_context()-> Result<(), Box>
}
#[test]
-#[cfg(feature = "context")]
-fn test_clear_context_command()-> Result<(), Box> {
- println!("π Testing /context clear command...");
-
- let test_file_path = "/tmp/test_context_file.py";
+#[cfg(all(feature = "context", feature = "sanity"))]
+fn test_add_remove_multiple_file_context()-> Result<(), Box> {
+ println!("\nπ Testing /context add command and /context remove ... | Description: Tests the /context add command with multiple files to verify batch context operations and /context remove command with multiple files to verify");
+ let test_file1_path = "/tmp/test_context_file1.py";
+ let test_file2_path = "/tmp/test_context_file2.py";
+ let test_file3_path = "/tmp/test_context_file.js";
// Create test files
- std::fs::write(test_file_path, "# Test Python file 1 for context\nprint('Hello from Python file 1')")?;
- println!("β Created test files at {}", test_file_path);
+ std::fs::write(test_file1_path, "# Test Python file 1 for context\nprint('Hello from Python file 1')")?;
+ std::fs::write(test_file2_path, "# Test Python file 2 for context\nprint('Hello from Python file 2')")?;
+ std::fs::write(test_file3_path, "// Test JavaScript file\nconsole.log('Hello from JS file');")?;
+ println!("β Created test files at {}, {}, {}", test_file1_path, test_file2_path, test_file3_path);
let session = get_chat_session();
let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
-
- // Add multiple files to context
- let add_response = chat.execute_command(&format!("/context add {}", test_file_path))?;
+
+ // Add multiple files to context in one command
+ let add_response = chat.execute_command(&format!("/context add {} {} {}", test_file1_path, test_file2_path, test_file3_path))?;
println!("π Context add response: {} bytes", add_response.len());
println!("π ADD RESPONSE:");
@@ -433,8 +438,8 @@ fn test_clear_context_command()-> Result<(), Box> {
println!("π END ADD RESPONSE");
// Verify files were added successfully - be flexible with the exact message format
- assert!(add_response.contains("Added") && (add_response.contains("1 path(s) to context") || add_response.contains("2 paths to context") || add_response.contains("2 files to context")), "Missing success message for adding files");
- println!("β Files added to context successfully");
+ assert!(add_response.contains("Added"), "Missing success message for adding multiple files");
+ println!("β Multiple files added to context successfully");
// Execute /context show to confirm files are present
let show_response = chat.execute_command("/context show")?;
@@ -444,23 +449,25 @@ fn test_clear_context_command()-> Result<(), Box> {
println!("{}", show_response);
println!("π END SHOW RESPONSE");
- // Verify files are present in context
- assert!(show_response.contains(test_file_path), "Python file not found in context show output");
- println!("β Files confirmed present in context");
-
- // Execute /context clear to remove all files
- let clear_response = chat.execute_command("/context clear")?;
+ // Verify all files are present in context
+ assert!(show_response.contains(test_file1_path), "Python file not found in context show output");
+ assert!(show_response.contains(test_file2_path), "JavaScript file not found in context show output");
+ assert!(show_response.contains(test_file3_path), "Text file not found in context show output");
+ println!("β All files confirmed present in context");
+
+ // Remove multiple files from context
+ let remove_response = chat.execute_command(&format!("/context remove {} {} {}", test_file1_path, test_file2_path, test_file3_path))?;
- println!("π Context clear response: {} bytes", clear_response.len());
- println!("π CLEAR RESPONSE:");
- println!("{}", clear_response);
- println!("π END CLEAR RESPONSE");
+ println!("π Context remove response: {} bytes", remove_response.len());
+ println!("π REMOVE RESPONSE:");
+ println!("{}", remove_response);
+ println!("π END REMOVE RESPONSE");
- // Verify context was cleared successfully
- assert!(clear_response.contains("Cleared context"), "Missing success message for clearing context");
- println!("β Context cleared successfully");
+ // Verify files were removed successfully - be flexible with the exact message format
+ assert!(remove_response.contains("Removed"), "Missing success message for removing multiple files");
+ println!("β Multiple files removed from context successfully");
- // Execute /context show to confirm no files remain
+ // Execute /context show to confirm files are gone
let final_show_response = chat.execute_command("/context show")?;
println!("π Final context show response: {} bytes", final_show_response.len());
@@ -468,18 +475,19 @@ fn test_clear_context_command()-> Result<(), Box> {
println!("{}", final_show_response);
println!("π END FINAL SHOW RESPONSE");
- // Verify no files remain in context
- assert!(!final_show_response.contains(test_file_path), "Python file still found in context after clear");
- assert!(final_show_response.contains("π€ Agent (q_cli_default):"), "Missing Agent section");
- assert!(final_show_response.contains("π¬ Session (temporary):"), "Missing Session section");
- assert!(final_show_response.contains(""), "Missing indicator for cleared context");
- println!("β All files confirmed removed from context and sections present");
+ // Verify files are no longer in context
+ assert!(!final_show_response.contains(test_file1_path), "Python file still found in context after removal");
+ assert!(!final_show_response.contains(test_file2_path), "JavaScript file still found in context after removal");
+ assert!(!final_show_response.contains(test_file3_path), "Text file still found in context after removal");
+ println!("β All files confirmed removed from context");
// Release the lock before cleanup
drop(chat);
// Clean up test file
- let _ = std::fs::remove_file(test_file_path);
+ let _ = std::fs::remove_file(test_file1_path);
+ let _ = std::fs::remove_file(test_file2_path);
+ let _ = std::fs::remove_file(test_file3_path);
println!("β Cleaned up test file");
// Cleanup only if this is the last test
@@ -489,25 +497,21 @@ fn test_clear_context_command()-> Result<(), Box> {
}
#[test]
-#[cfg(feature = "context")]
-fn test_add_remove_multiple_file_context()-> Result<(), Box> {
- println!("π Testing /context add command and /context remove ...");
-
- let test_file1_path = "/tmp/test_context_file1.py";
- let test_file2_path = "/tmp/test_context_file2.py";
- let test_file3_path = "/tmp/test_context_file.js";
+#[cfg(all(feature = "context", feature = "sanity"))]
+fn test_clear_context_command()-> Result<(), Box> {
+ println!("\nπ Testing /context clear command... | Description: Tests the /context clear command to remove all files from context and verify the context is completely cleared");
+
+ let test_file_path = "/tmp/test_context_file.py";
// Create test files
- std::fs::write(test_file1_path, "# Test Python file 1 for context\nprint('Hello from Python file 1')")?;
- std::fs::write(test_file2_path, "# Test Python file 2 for context\nprint('Hello from Python file 2')")?;
- std::fs::write(test_file3_path, "// Test JavaScript file\nconsole.log('Hello from JS file');")?;
- println!("β Created test files at {}, {}, {}", test_file1_path, test_file2_path, test_file3_path);
+ std::fs::write(test_file_path, "# Test Python file 1 for context\nprint('Hello from Python file 1')")?;
+ println!("β Created test files at {}", test_file_path);
let session = get_chat_session();
let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
-
- // Add multiple files to context in one command
- let add_response = chat.execute_command(&format!("/context add {} {} {}", test_file1_path, test_file2_path, test_file3_path))?;
+
+ // Add multiple files to context
+ let add_response = chat.execute_command(&format!("/context add {}", test_file_path))?;
println!("π Context add response: {} bytes", add_response.len());
println!("π ADD RESPONSE:");
@@ -515,8 +519,8 @@ fn test_add_remove_multiple_file_context()-> Result<(), Box Result<(), Box Result<(), Box"), "Missing indicator for cleared context");
+ println!("β All files confirmed removed from context and sections present");
// Release the lock before cleanup
drop(chat);
// Clean up test file
- let _ = std::fs::remove_file(test_file1_path);
- let _ = std::fs::remove_file(test_file2_path);
- let _ = std::fs::remove_file(test_file3_path);
+ let _ = std::fs::remove_file(test_file_path);
println!("β Cleaned up test file");
// Cleanup only if this is the last test
@@ -573,4 +572,3 @@ fn test_add_remove_multiple_file_context()-> Result<(), Box> = None;
+
+#[allow(dead_code)]
+pub fn get_chat_session() -> &'static Mutex {
+ unsafe {
+ INIT.call_once(|| {
+ let chat = q_chat_helper::QChatSession::new().expect("Failed to create chat session");
+ println!("β Q Chat session started");
+ CHAT_SESSION = Some(Mutex::new(chat));
+ });
+ (&raw const CHAT_SESSION).as_ref().unwrap().as_ref().unwrap()
+ }
+}
+
+#[allow(dead_code)]
+pub fn cleanup_if_last_test(test_count: &AtomicUsize, total_tests: usize) -> Result> {
+ let count = test_count.fetch_add(1, Ordering::SeqCst) + 1;
+ if count == total_tests {
+ unsafe {
+ if let Some(session) = (&raw const CHAT_SESSION).as_ref().unwrap() {
+ if let Ok(mut chat) = session.lock() {
+ chat.quit()?;
+ println!("β Test completed successfully");
+ }
+ }
+ }
+ }
+ Ok(count)
+}
+#[allow(dead_code)]
+static TEST_COUNT: AtomicUsize = AtomicUsize::new(0);
+
+// List of covered tests
+#[allow(dead_code)]
+const TEST_NAMES: &[&str] = &[
+ "test_clear_command",
+];
+#[allow(dead_code)]
+const TOTAL_TESTS: usize = TEST_NAMES.len();
+
+#[test]
+#[cfg(all(feature = "clear", feature = "sanity"))]
+fn test_clear_command() -> Result<(), Box> {
+ println!("\nπ Testing /clear command... | Description: Tests the /clear command to clear conversation history and verify that previous context is no longer remembered by the AI");
+
+ let session = get_chat_session();
+ let mut chat = session.lock().unwrap();
+
+ println!("β Q Chat session started");
+
+ // Send initial message
+ println!("\nπ Sending prompt: 'My name is TestUser'");
+ let _initial_response = chat.execute_command("My name is TestUser")?;
+ println!("π Initial response: {} bytes", _initial_response.len());
+ println!("π INITIAL RESPONSE OUTPUT:");
+ println!("{}", _initial_response);
+ println!("π END INITIAL RESPONSE");
+
+ // Execute clear command
+ println!("\nπ Executing command: '/clear'");
+ let _clear_response = chat.execute_command("/clear")?;
+
+ println!("β Clear command executed");
+
+ // Check if AI remembers previous conversation
+ println!("\nπ Sending prompt: 'What is my name?'");
+ let test_response = chat.execute_command("What is my name?")?;
+ println!("π Test response: {} bytes", test_response.len());
+ println!("π TEST RESPONSE OUTPUT:");
+ println!("{}", test_response);
+ println!("π END TEST RESPONSE");
+
+ // Verify history is cleared - AI shouldn't remember the name
+ assert!(!test_response.to_lowercase().contains("testuser"), "Clear command failed - AI still remembers previous conversation");
+ println!("β Clear command successful - Conversation history cleared.");
+
+ // Release the lock before cleanup
+ drop(chat);
+
+ // Cleanup only if this is the last test
+ cleanup_if_last_test(&TEST_COUNT, TOTAL_TESTS)?;
+
+ Ok(())
+}
\ No newline at end of file
diff --git a/e2etests/tests/core_session/test_help_command.rs b/e2etests/tests/core_session/test_help_command.rs
new file mode 100644
index 0000000000..3b7f66269a
--- /dev/null
+++ b/e2etests/tests/core_session/test_help_command.rs
@@ -0,0 +1,94 @@
+#[allow(unused_imports)]
+use q_cli_e2e_tests::q_chat_helper;
+use std::sync::{Mutex, Once, atomic::{AtomicUsize, Ordering}};
+#[allow(dead_code)]
+static INIT: Once = Once::new();
+#[allow(dead_code)]
+static mut CHAT_SESSION: Option> = None;
+
+#[allow(dead_code)]
+pub fn get_chat_session() -> &'static Mutex {
+ unsafe {
+ INIT.call_once(|| {
+ let chat = q_chat_helper::QChatSession::new().expect("Failed to create chat session");
+ println!("β Q Chat session started");
+ CHAT_SESSION = Some(Mutex::new(chat));
+ });
+ (&raw const CHAT_SESSION).as_ref().unwrap().as_ref().unwrap()
+ }
+}
+
+#[allow(dead_code)]
+pub fn cleanup_if_last_test(test_count: &AtomicUsize, total_tests: usize) -> Result> {
+ let count = test_count.fetch_add(1, Ordering::SeqCst) + 1;
+ if count == total_tests {
+ unsafe {
+ if let Some(session) = (&raw const CHAT_SESSION).as_ref().unwrap() {
+ if let Ok(mut chat) = session.lock() {
+ chat.quit()?;
+ println!("β Test completed successfully");
+ }
+ }
+ }
+ }
+ Ok(count)
+}
+#[allow(dead_code)]
+static TEST_COUNT: AtomicUsize = AtomicUsize::new(0);
+
+// List of covered tests
+#[allow(dead_code)]
+const TEST_NAMES: &[&str] = &[
+ "test_help_command",
+];
+#[allow(dead_code)]
+const TOTAL_TESTS: usize = TEST_NAMES.len();
+
+#[test]
+#[cfg(all(feature = "help", feature = "sanity"))]
+fn test_help_command() -> Result<(), Box> {
+ println!("\nπ Testing /help command... | Description: Tests the /help command to display all available commands and verify core functionality like quit, clear, tools, and help commands are present");
+
+ let session = get_chat_session();
+ let mut chat = session.lock().unwrap();
+
+ println!("β Q Chat session started");
+
+ let response = chat.execute_command("/help")?;
+
+ println!("π Help response: {} bytes", response.len());
+ println!("π FULL OUTPUT:");
+ println!("{}", response);
+ println!("π END OUTPUT");
+
+ // Verify help content
+ assert!(response.contains("Commands:"), "Missing Commands section");
+ println!("β Found Commands section with all available commands");
+
+ assert!(response.contains("quit"), "Missing quit command");
+ assert!(response.contains("clear"), "Missing clear command");
+ assert!(response.contains("tools"), "Missing tools command");
+ assert!(response.contains("help"), "Missing help command");
+ println!("β Verified core commands: quit, clear, tools, help");
+
+ // Verify specific useful commands
+ if response.contains("context") {
+ println!("β Found context management command");
+ }
+ if response.contains("agent") {
+ println!("β Found agent management command");
+ }
+ if response.contains("model") {
+ println!("β Found model selection command");
+ }
+
+ println!("β All help content verified!");
+
+ // Release the lock before cleanup
+ drop(chat);
+
+ // Cleanup only if this is the last test
+ cleanup_if_last_test(&TEST_COUNT, TOTAL_TESTS)?;
+
+ Ok(())
+}
diff --git a/e2etests/tests/core_session/test_quit_command.rs b/e2etests/tests/core_session/test_quit_command.rs
new file mode 100644
index 0000000000..e253f5b191
--- /dev/null
+++ b/e2etests/tests/core_session/test_quit_command.rs
@@ -0,0 +1,48 @@
+#[allow(unused_imports)]
+use q_cli_e2e_tests::q_chat_helper;
+use std::sync::{Mutex, Once, atomic::{AtomicUsize}};
+#[allow(dead_code)]
+static INIT: Once = Once::new();
+#[allow(dead_code)]
+static mut CHAT_SESSION: Option> = None;
+
+#[allow(dead_code)]
+pub fn get_chat_session() -> &'static Mutex {
+ unsafe {
+ INIT.call_once(|| {
+ let chat = q_chat_helper::QChatSession::new().expect("Failed to create chat session");
+ println!("β Q Chat session started");
+ CHAT_SESSION = Some(Mutex::new(chat));
+ });
+ (&raw const CHAT_SESSION).as_ref().unwrap().as_ref().unwrap()
+ }
+}
+
+#[allow(dead_code)]
+static TEST_COUNT: AtomicUsize = AtomicUsize::new(0);
+
+// List of covered tests
+#[allow(dead_code)]
+const TEST_NAMES: &[&str] = &[
+ "test_quit_command",
+];
+#[allow(dead_code)]
+const TOTAL_TESTS: usize = TEST_NAMES.len();
+
+#[test]
+#[cfg(all(feature = "quit", feature = "sanity"))]
+fn test_quit_command() -> Result<(), Box> {
+ println!("\nπ Testing /quit command... | Description: Tests the /quit command to properly terminate the Q Chat session and exit cleanly");
+
+ let session = get_chat_session();
+ let mut chat = session.lock().unwrap();
+
+ println!("β Q Chat session started");
+
+ chat.execute_command("/quit")?;
+
+ println!("β /quit command executed successfully");
+ println!("β Test completed successfully");
+
+ Ok(())
+}
diff --git a/e2etests/tests/integration/mod.rs b/e2etests/tests/integration/mod.rs
new file mode 100644
index 0000000000..844f370a00
--- /dev/null
+++ b/e2etests/tests/integration/mod.rs
@@ -0,0 +1,4 @@
+pub mod test_editor_help_command;
+pub mod test_hooks_command;
+pub mod test_issue_command;
+pub mod test_subscribe_command;
\ No newline at end of file
diff --git a/e2etests/tests/integration/test_editor_help_command.rs b/e2etests/tests/integration/test_editor_help_command.rs
new file mode 100644
index 0000000000..ed7cac3fbf
--- /dev/null
+++ b/e2etests/tests/integration/test_editor_help_command.rs
@@ -0,0 +1,340 @@
+#[allow(unused_imports)]
+use q_cli_e2e_tests::q_chat_helper;
+use std::sync::{Mutex, Once, atomic::{AtomicUsize, Ordering}};
+
+#[allow(dead_code)]
+static TEST_COUNT: AtomicUsize = AtomicUsize::new(0);
+
+// List of covered tests
+#[allow(dead_code)]
+const TEST_NAMES: &[&str] = &[
+ "test_editor_help_command",
+ "test_help_editor_command",
+ "test_editor_h_command",
+ "test_editor_command_interaction",
+ "test_editor_command_error",
+ "test_editor_with_file_path",
+];
+#[allow(dead_code)]
+const TOTAL_TESTS: usize = TEST_NAMES.len();
+
+#[allow(dead_code)]
+static INIT: Once = Once::new();
+#[allow(dead_code)]
+static mut CHAT_SESSION: Option> = None;
+
+#[allow(dead_code)]
+pub fn get_chat_session() -> &'static Mutex {
+ unsafe {
+ INIT.call_once(|| {
+ let chat = q_chat_helper::QChatSession::new().expect("Failed to create chat session");
+ println!("β Q Chat session started");
+ CHAT_SESSION = Some(Mutex::new(chat));
+ });
+ (&raw const CHAT_SESSION).as_ref().unwrap().as_ref().unwrap()
+ }
+}
+
+#[allow(dead_code)]
+pub fn cleanup_if_last_test(test_count: &AtomicUsize, total_tests: usize) -> Result> {
+ let count = test_count.fetch_add(1, Ordering::SeqCst) + 1;
+ if count == total_tests {
+ unsafe {
+ if let Some(session) = (&raw const CHAT_SESSION).as_ref().unwrap() {
+ if let Ok(mut chat) = session.lock() {
+ chat.quit()?;
+ println!("β Test completed successfully");
+ }
+ }
+ }
+ }
+ Ok(count)
+}
+
+#[test]
+#[cfg(all(feature = "editor", feature = "sanity"))]
+fn test_editor_help_command() -> Result<(), Box> {
+ println!("\nπ Testing /editor --help command... | Description: Tests the /editor --help command to display help information for the editor functionality including usage and options");
+
+ let session = get_chat_session();
+ let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
+
+ let response = chat.execute_command("/editor --help")?;
+
+ println!("π Editor help response: {} bytes", response.len());
+ println!("π FULL OUTPUT:");
+ println!("{}", response);
+ println!("π END OUTPUT");
+
+ // Verify Usage section
+ assert!(response.contains("Usage:") && response.contains("/editor") && response.contains("[INITIAL_TEXT]"), "Missing Usage section");
+ println!("β Found Usage section with /editor command");
+
+ // Verify Arguments section
+ assert!(response.contains("Arguments:"), "Missing Arguments section");
+ assert!(response.contains("[INITIAL_TEXT]"), "Missing INITIAL_TEXT argument");
+ println!("β Found Arguments section");
+
+ // Verify Options section
+ assert!(response.contains("Options:"), "Missing Options section");
+ println!("β Found Options section");
+
+ // Verify help flags
+ assert!(response.contains("-h") && response.contains("--help"), "Missing -h, --help flags");
+ println!("β Found help flags: -h, --help with Print help description");
+
+ println!("β All editor help content verified!");
+
+ // Release the lock before cleanup
+ drop(chat);
+
+ // Cleanup only if this is the last test
+ cleanup_if_last_test(&TEST_COUNT, TOTAL_TESTS)?;
+
+ Ok(())
+}
+
+#[test]
+#[cfg(all(feature = "editor", feature = "sanity"))]
+fn test_help_editor_command() -> Result<(), Box> {
+ println!("\nπ Testing /help editor command... | Description: Tests the /help editor command to display editor-specific help information and usage instructions");
+
+ let session = get_chat_session();
+ let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
+
+ let response = chat.execute_command("/help editor")?;
+
+ println!("π Help editor response: {} bytes", response.len());
+ println!("π FULL OUTPUT:");
+ println!("{}", response);
+ println!("π END OUTPUT");
+
+ // Verify Usage section
+ assert!(response.contains("Usage:") && response.contains("/editor") && response.contains("[INITIAL_TEXT]"), "Missing Usage section");
+ println!("β Found Usage section with /editor command");
+
+ // Verify Arguments section
+ assert!(response.contains("Arguments:"), "Missing Arguments section");
+ assert!(response.contains("[INITIAL_TEXT]"), "Missing INITIAL_TEXT argument");
+ println!("β Found Arguments section");
+
+ // Verify Options section
+ assert!(response.contains("Options:"), "Missing Options section");
+ println!("β Found Options section");
+
+ // Verify help flags
+ assert!(response.contains("-h") && response.contains("--help"), "Missing -h, --help flags");
+ println!("β Found help flags: -h, --help with Print help description");
+
+ println!("β All editor help content verified!");
+
+ // Release the lock before cleanup
+ drop(chat);
+
+ // Cleanup only if this is the last test
+ cleanup_if_last_test(&TEST_COUNT, TOTAL_TESTS)?;
+
+ Ok(())
+}
+
+#[test]
+#[cfg(all(feature = "editor", feature = "sanity"))]
+fn test_editor_h_command() -> Result<(), Box> {
+ println!("\nπ Testing /editor -h command... | Description: Tests the /editor -h command (short form) to display editor help information and verify proper flag handling");
+
+ let session = get_chat_session();
+ let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
+
+ let response = chat.execute_command("/editor -h")?;
+
+ println!("π Editor help response: {} bytes", response.len());
+ println!("π FULL OUTPUT:");
+ println!("{}", response);
+ println!("π END OUTPUT");
+
+ // Verify Usage section
+ assert!(response.contains("Usage:") && response.contains("/editor") && response.contains("[INITIAL_TEXT]"), "Missing Usage section");
+ println!("β Found Usage section with /editor command");
+
+ // Verify Arguments section
+ assert!(response.contains("Arguments:"), "Missing Arguments section");
+ assert!(response.contains("[INITIAL_TEXT]"), "Missing INITIAL_TEXT argument");
+ println!("β Found Arguments section");
+
+ // Verify Options section
+ assert!(response.contains("Options:"), "Missing Options section");
+ println!("β Found Options section");
+
+ // Verify help flags
+ assert!(response.contains("-h") && response.contains("--help"), "Missing -h, --help flags");
+ println!("β Found help flags: -h, --help with Print help description");
+
+ println!("β All editor help content verified!");
+
+ // Release the lock before cleanup
+ drop(chat);
+
+ // Cleanup only if this is the last test
+ cleanup_if_last_test(&TEST_COUNT, TOTAL_TESTS)?;
+
+ Ok(())
+}
+
+#[test]
+#[cfg(all(feature = "editor", feature = "sanity"))]
+fn test_editor_command_interaction() -> Result<(), Box> {
+ println!("π Testing /editor command interaction... | Description: Test that the /editor command successfully launches the integrated editor interface");
+
+ let session = get_chat_session();
+ let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
+
+ // Execute /editor command to open editor panel
+ let response = chat.execute_command("/editor")?;
+
+ println!("π Editor command response: {} bytes", response.len());
+ println!("π EDITOR RESPONSE:");
+ println!("{}", response);
+ println!("π END EDITOR RESPONSE");
+
+ // Press 'i' to enter insert mode
+ let insert_response = chat.execute_command("i")?;
+ println!("π Insert mode response: {} bytes", insert_response.len());
+
+ // Type "what is aws?"
+ let type_response = chat.execute_command("what is aws?")?;
+ println!("π Type response: {} bytes", type_response.len());
+
+ // Press Esc to exit insert mode
+ let esc_response = chat.execute_command("\x1b")?; // ESC key
+ println!("π Esc response: {} bytes", esc_response.len());
+
+ // Execute :wq to save and quit
+ let wq_response = chat.execute_command(":wq")?;
+
+ println!("π Final wq response: {} bytes", wq_response.len());
+ println!("π WQ RESPONSE:");
+ println!("{}", wq_response);
+ println!("π END WQ RESPONSE");
+
+ // Verify expected output
+ assert!(wq_response.contains("Content loaded from editor. Submitting prompt..."), "Missing expected editor output message");
+ println!("β Found expected editor output: 'Content loaded from editor. Submitting prompt...'");
+
+ println!("β Editor command interaction test completed successfully!");
+
+ // Release the lock before cleanup
+ drop(chat);
+
+ // Cleanup only if this is the last test
+ cleanup_if_last_test(&TEST_COUNT, TOTAL_TESTS)?;
+
+ Ok(())
+}
+
+#[test]
+#[cfg(all(feature = "editor", feature = "sanity"))]
+fn test_editor_command_error() -> Result<(), Box> {
+ println!("π Testing /editor command error handling ... | Description: Tests the /editor command error handling when attempting to open a nonexistent file");
+
+ let session = get_chat_session();
+ let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
+
+ // Execute /editor command to open editor panel
+ let response = chat.execute_command("/editor nonexistent_file.txt")?;
+
+ println!("π Editor command response: {} bytes", response.len());
+ println!("π EDITOR RESPONSE:");
+ println!("{}", response);
+ println!("π END EDITOR RESPONSE");
+
+ // Press 'i' to enter insert mode
+ let insert_response = chat.execute_command("i")?;
+ println!("π Insert mode response: {} bytes", insert_response.len());
+
+
+ // Press Esc to exit insert mode
+ let esc_response = chat.execute_command("\x1b")?; // ESC key
+ println!("π Esc response: {} bytes", esc_response.len());
+
+ // Execute :wq to save and quit
+ let wq_response = chat.execute_command(":wq")?;
+
+ println!("π Final wq response: {} bytes", wq_response.len());
+ println!("π WQ RESPONSE:");
+ println!("{}", wq_response);
+ println!("π END WQ RESPONSE");
+
+ // Verify expected output
+ assert!(wq_response.contains("Content loaded from editor. Submitting prompt..."), "Missing expected editor output message");
+ println!("β Found expected editor output: 'Content loaded from editor. Submitting prompt...'");
+
+ assert!(wq_response.contains("nonexistent_file.txt") && wq_response.contains("does not exist"), "Missing file validation error message");
+ println!("β Found expected file validation error message");
+
+ println!("β Editor command error test completed successfully!");
+
+ // Release the lock before cleanup
+ drop(chat);
+
+ // Cleanup only if this is the last test
+ cleanup_if_last_test(&TEST_COUNT, TOTAL_TESTS)?;
+
+ Ok(())
+}
+
+#[test]
+#[cfg(all(feature = "editor", feature = "sanity"))]
+fn test_editor_with_file_path() -> Result<(), Box> {
+ println!("π Testing /editor command... | Description: Tests the /editor command to load an existing file into the editor and verify content loading");
+
+ let home_dir = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
+ let test_file_path = format!("{}/test_editor_file.txt", home_dir);
+
+ // Create a test file
+ std::fs::write(&test_file_path, "Hello from test file\nThis is a test file for editor command.")?;
+ println!("β Created test file at {}", test_file_path);
+
+ let session = get_chat_session();
+ let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
+
+ // Execute /editor command with file path
+ let response = chat.execute_command(&format!("/editor {}", test_file_path))?;
+
+ println!("π Editor with file response: {} bytes", response.len());
+ println!("π FULL OUTPUT:");
+ println!("{}", response);
+ println!("π END OUTPUT");
+
+ // Press 'i' to enter insert mode
+ let insert_response = chat.execute_command("i")?;
+ println!("π Insert mode response: {} bytes", insert_response.len());
+
+
+ // Press Esc to exit insert mode
+ let esc_response = chat.execute_command("\x1b")?; // ESC key
+ println!("π Esc response: {} bytes", esc_response.len());
+
+ // Execute :wq to save and quit
+ let wq_response = chat.execute_command(":wq")?;
+
+ println!("π Final wq response: {} bytes", wq_response.len());
+ println!("π WQ RESPONSE:");
+ println!("{}", wq_response);
+ println!("π END WQ RESPONSE");
+
+ // Verify the file content is loaded in editor
+ assert!(wq_response.contains("Hello from test file"), "File content not loaded in editor");
+ println!("β File content loaded successfully in editor");
+
+ // Clean up test file
+ std::fs::remove_file(test_file_path).ok();
+ println!("β Cleaned up test file");
+
+ // Release the lock before cleanup
+ drop(chat);
+
+ // Cleanup only if this is the last test
+ cleanup_if_last_test(&TEST_COUNT, TOTAL_TESTS)?;
+
+ Ok(())
+}
\ No newline at end of file
diff --git a/e2etests/tests/integration/test_hooks_command.rs b/e2etests/tests/integration/test_hooks_command.rs
new file mode 100644
index 0000000000..15cb2e9f43
--- /dev/null
+++ b/e2etests/tests/integration/test_hooks_command.rs
@@ -0,0 +1,156 @@
+#[allow(unused_imports)]
+use q_cli_e2e_tests::q_chat_helper;
+use std::sync::{Mutex, Once, atomic::{AtomicUsize, Ordering}};
+#[allow(dead_code)]
+static INIT: Once = Once::new();
+#[allow(dead_code)]
+static mut CHAT_SESSION: Option> = None;
+
+#[allow(dead_code)]
+pub fn get_chat_session() -> &'static Mutex {
+ unsafe {
+ INIT.call_once(|| {
+ let chat = q_chat_helper::QChatSession::new().expect("Failed to create chat session");
+ println!("β Q Chat session started");
+ CHAT_SESSION = Some(Mutex::new(chat));
+ });
+ (&raw const CHAT_SESSION).as_ref().unwrap().as_ref().unwrap()
+ }
+}
+
+#[allow(dead_code)]
+pub fn cleanup_if_last_test(test_count: &AtomicUsize, total_tests: usize) -> Result> {
+ let count = test_count.fetch_add(1, Ordering::SeqCst) + 1;
+ if count == total_tests {
+ unsafe {
+ if let Some(session) = (&raw const CHAT_SESSION).as_ref().unwrap() {
+ if let Ok(mut chat) = session.lock() {
+ chat.quit()?;
+ println!("β Test completed successfully");
+ }
+ }
+ }
+ }
+ Ok(count)
+}
+
+#[allow(dead_code)]
+static TEST_COUNT: AtomicUsize = AtomicUsize::new(0);
+
+// List of covered tests
+#[allow(dead_code)]
+const TEST_NAMES: &[&str] = &[
+ "test_hooks_command",
+ "test_hooks_help_command",
+ "test_hooks_h_command",
+];
+#[allow(dead_code)]
+const TOTAL_TESTS: usize = TEST_NAMES.len();
+
+#[test]
+#[cfg(all(feature = "hooks", feature = "sanity"))]
+fn test_hooks_command() -> Result<(), Box> {
+ println!("\nπ Testing /hooks command... | Description: Tests the /hooks command to display configured hooks or show no hooks message when none are configured");
+
+ let session = get_chat_session();
+ let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
+
+ let response = chat.execute_command("/hooks")?;
+
+ println!("π Hooks command response: {} bytes", response.len());
+ println!("π FULL OUTPUT:");
+ println!("{}", response);
+ println!("π END OUTPUT");
+
+ // Verify no hooks configured message
+ assert!(response.contains("No hooks"), "Missing no hooks configured message");
+ println!("β Found no hooks configured message");
+
+ println!("β All hooks command functionality verified!");
+
+ // Release the lock before cleanup
+ drop(chat);
+
+ // Cleanup only if this is the last test
+ cleanup_if_last_test(&TEST_COUNT, TOTAL_TESTS)?;
+
+ Ok(())
+}
+
+#[test]
+#[cfg(all(feature = "hooks", feature = "sanity"))]
+fn test_hooks_help_command() -> Result<(), Box> {
+ println!("\nπ Testing /hooks --help command... | Description: Tests the /hooks --help command to display comprehensive help information for hooks functionality and configuration");
+
+ let session = get_chat_session();
+ let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
+
+ let response = chat.execute_command("/hooks --help")?;
+
+ println!("π Hooks help response: {} bytes", response.len());
+ println!("π FULL OUTPUT:");
+ println!("{}", response);
+ println!("π END OUTPUT");
+
+ // Verify Usage section
+ assert!(response.contains("Usage:"), "Missing Usage section");
+ assert!(response.contains("/hooks"), "Missing /hooks command in usage section");
+ println!("β Found Usage section with /hooks command");
+
+ // Verify Options section
+ assert!(response.contains("Options:"), "Missing Options section");
+ println!("β Found Options section");
+
+ // Verify help flags
+ assert!(response.contains("-h") && response.contains("--help"), "Missing -h, --help flags");
+ println!("β Found help flags: -h, --help with Print help description");
+
+ println!("β All hooks help content verified!");
+
+ // Release the lock before cleanup
+ drop(chat);
+
+ // Cleanup only if this is the last test
+ cleanup_if_last_test(&TEST_COUNT, TOTAL_TESTS)?;
+
+ Ok(())
+}
+
+#[test]
+#[cfg(all(feature = "hooks", feature = "sanity"))]
+fn test_hooks_h_command() -> Result<(), Box> {
+ println!("\nπ Testing /hooks -h command... | Description: Tests the /hooks -h command (short form) to display hooks help information and verify flag handling");
+
+ let session = get_chat_session();
+ let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
+
+ let response = chat.execute_command("/hooks -h")?;
+
+ println!("π Hooks help response: {} bytes", response.len());
+ println!("π FULL OUTPUT:");
+ println!("{}", response);
+ println!("π END OUTPUT");
+
+ // Verify Usage section
+ assert!(response.contains("Usage:"), "Missing Usage section");
+ assert!(response.contains("/hooks"), "Missing /hooks command in usage section");
+ println!("β Found Usage section with /hooks command");
+
+ // Verify Options section
+ assert!(response.contains("Options:"), "Missing Options section");
+ println!("β Found Options section");
+
+ // Verify help flags
+ assert!(response.contains("-h") && response.contains("--help"), "Missing -h, --help flags");
+ println!("β Found help flags: -h, --help with Print help description");
+
+ println!("β All hooks help content verified!");
+
+ // Release the lock before cleanup
+ drop(chat);
+
+ // Cleanup only if this is the last test
+ cleanup_if_last_test(&TEST_COUNT, TOTAL_TESTS)?;
+
+ Ok(())
+}
\ No newline at end of file
diff --git a/e2etests/tests/integration/test_issue_command.rs b/e2etests/tests/integration/test_issue_command.rs
new file mode 100644
index 0000000000..531dec3e4d
--- /dev/null
+++ b/e2etests/tests/integration/test_issue_command.rs
@@ -0,0 +1,221 @@
+#[allow(unused_imports)]
+use q_cli_e2e_tests::q_chat_helper;
+use std::sync::{Mutex, Once, atomic::{AtomicUsize, Ordering}};
+#[allow(dead_code)]
+static INIT: Once = Once::new();
+#[allow(dead_code)]
+static mut CHAT_SESSION: Option> = None;
+
+#[allow(dead_code)]
+pub fn get_chat_session() -> &'static Mutex {
+ unsafe {
+ INIT.call_once(|| {
+ let chat = q_chat_helper::QChatSession::new().expect("Failed to create chat session");
+ println!("β Q Chat session started");
+ CHAT_SESSION = Some(Mutex::new(chat));
+ });
+ (&raw const CHAT_SESSION).as_ref().unwrap().as_ref().unwrap()
+ }
+}
+
+#[allow(dead_code)]
+pub fn cleanup_if_last_test(test_count: &AtomicUsize, total_tests: usize) -> Result> {
+ let count = test_count.fetch_add(1, Ordering::SeqCst) + 1;
+ if count == total_tests {
+ unsafe {
+ if let Some(session) = (&raw const CHAT_SESSION).as_ref().unwrap() {
+ if let Ok(mut chat) = session.lock() {
+ chat.quit()?;
+ println!("β Test completed successfully");
+ }
+ }
+ }
+ }
+ Ok(count)
+}
+
+#[allow(dead_code)]
+static TEST_COUNT: AtomicUsize = AtomicUsize::new(0);
+
+// List of covered tests
+#[allow(dead_code)]
+const TEST_NAMES: &[&str] = &[
+ "test_issue_command",
+ "test_issue_force_command",
+ "test_issue_f_command",
+ "test_issue_help_command",
+ "test_issue_h_command",
+];
+#[allow(dead_code)]
+const TOTAL_TESTS: usize = TEST_NAMES.len();
+
+#[test]
+#[cfg(all(feature = "issue_reporting", feature = "sanity"))]
+fn test_issue_command() -> Result<(), Box> {
+ println!("\nπ Testing /issue command with bug report... | Description: Tests the /issue command to create a bug report and verify it opens GitHub issue creation interface");
+
+ let session = get_chat_session();
+ let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
+
+ let response = chat.execute_command("/issue \"Bug: Q CLI crashes when using large files\"")?;
+
+ println!("π Issue command response: {} bytes", response.len());
+ println!("π FULL OUTPUT:");
+ println!("{}", response);
+ println!("π END OUTPUT");
+
+ // Verify command executed successfully (GitHub opens automatically)
+ assert!(response.contains("Heading over to GitHub..."), "Missing browser opening confirmation");
+ println!("β Found browser opening confirmation");
+
+ println!("β All issue command functionality verified!");
+
+ // Release the lock before cleanup
+ drop(chat);
+
+ // Cleanup only if this is the last test
+ cleanup_if_last_test(&TEST_COUNT, TOTAL_TESTS)?;
+
+ Ok(())
+}
+
+#[test]
+#[cfg(all(feature = "issue_reporting", feature = "sanity"))]
+fn test_issue_force_command() -> Result<(), Box> {
+ println!("\nπ Testing /issue --force command with critical bug... | Description: Tests the /issue --force command to create a critical bug report and verify forced issue creation workflow");
+
+ let session = get_chat_session();
+ let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
+
+ let response = chat.execute_command("/issue --force \"Critical bug in file handling\"")?;
+
+ println!("π Issue force command response: {} bytes", response.len());
+ println!("π FULL OUTPUT:");
+ println!("{}", response);
+ println!("π END OUTPUT");
+
+ // Verify command executed successfully (GitHub opens automatically)
+ assert!(response.contains("Heading over to GitHub..."), "Missing browser opening confirmation");
+ println!("β Found browser opening confirmation");
+
+ println!("β All issue --force command functionality verified!");
+
+ // Release the lock before cleanup
+ drop(chat);
+
+ // Cleanup only if this is the last test
+ cleanup_if_last_test(&TEST_COUNT, TOTAL_TESTS)?;
+
+ Ok(())
+}
+
+#[test]
+#[cfg(all(feature = "issue_reporting", feature = "sanity"))]
+fn test_issue_f_command() -> Result<(), Box> {
+ println!("\nπ Testing /issue -f command with critical bug... | Description: Tests the /issue -f command (short form) to create a critical bug report with force flag");
+
+ let session = get_chat_session();
+ let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
+
+ let response = chat.execute_command("/issue -f \"Critical bug in file handling\"")?;
+
+ println!("π Issue force command response: {} bytes", response.len());
+ println!("π FULL OUTPUT:");
+ println!("{}", response);
+ println!("π END OUTPUT");
+
+ // Verify command executed successfully (GitHub opens automatically)
+ assert!(response.contains("Heading over to GitHub..."), "Missing browser opening confirmation");
+ println!("β Found browser opening confirmation");
+
+ println!("β All issue --force command functionality verified!");
+
+ // Release the lock before cleanup
+ drop(chat);
+
+ // Cleanup only if this is the last test
+ cleanup_if_last_test(&TEST_COUNT, TOTAL_TESTS)?;
+
+ Ok(())
+}
+
+
+#[test]
+#[cfg(all(feature = "issue_reporting", feature = "sanity"))]
+fn test_issue_help_command() -> Result<(), Box> {
+ println!("\nπ Testing /issue --help command... | Description: Tests the /issue --help command to display help information for issue reporting functionality including options and usage");
+
+ let session = get_chat_session();
+ let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
+
+ let response = chat.execute_command("/issue --help")?;
+
+ println!("π Issue help response: {} bytes", response.len());
+ println!("π FULL OUTPUT:");
+ println!("{}", response);
+ println!("π END OUTPUT");
+
+ // Verify Usage section
+ assert!(response.contains("Usage:") && response.contains("/issue") && response.contains("[DESCRIPTION]") && response.contains("[OPTIONS]"), "Missing Usage section");
+ println!("β Found usage format");
+
+ // Verify Arguments section
+ assert!(response.contains("Arguments:"), "Missing Arguments section");
+ println!("β Found Arguments section");
+
+ // Verify Options section
+ assert!(response.contains("Options:"), "Missing Options section");
+ assert!(response.contains("-f") && response.contains("--force"), "Missing force option");
+ assert!(response.contains("-h") && response.contains("--help"), "Missing -h, --help flags");
+ println!("β Found Options section with force and help flags");
+
+ println!("β All issue help content verified!");
+
+ // Release the lock before cleanup
+ drop(chat);
+
+ // Cleanup only if this is the last test
+ cleanup_if_last_test(&TEST_COUNT, TOTAL_TESTS)?;
+
+ Ok(())
+}
+
+#[test]
+#[cfg(all(feature = "issue_reporting", feature = "sanity"))]
+fn test_issue_h_command() -> Result<(), Box> {
+ println!("\nπ Testing /issue -h command... | Description: Tests the /issue -h command (short form) to display issue reporting help information");
+
+ let session = get_chat_session();
+ let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
+
+ let response = chat.execute_command("/issue -h")?;
+
+ println!("π Issue help response: {} bytes", response.len());
+ println!("π FULL OUTPUT:");
+ println!("{}", response);
+ println!("π END OUTPUT");
+
+ // Verify Usage section
+ assert!(response.contains("Usage:") && response.contains("/issue") && response.contains("[DESCRIPTION]") && response.contains("[OPTIONS]"), "Missing Usage section");
+ println!("β Found usage format");
+
+ // Verify Arguments section
+ assert!(response.contains("Arguments:"), "Missing Arguments section");
+ println!("β Found Arguments section");
+
+ // Verify Options section
+ assert!(response.contains("Options:"), "Missing Options section");
+ assert!(response.contains("-f") && response.contains("--force"), "Missing force option");
+ assert!(response.contains("-h") && response.contains("--help"), "Missing -h, --help flags");
+ println!("β Found Options section with force and help flags");
+
+ println!("β All issue help content verified!");
+
+ // Release the lock before cleanup
+ drop(chat);
+
+ // Cleanup only if this is the last test
+ cleanup_if_last_test(&TEST_COUNT, TOTAL_TESTS)?;
+
+ Ok(())
+}
\ No newline at end of file
diff --git a/e2etests/tests/integration/test_subscribe_command.rs b/e2etests/tests/integration/test_subscribe_command.rs
new file mode 100644
index 0000000000..7038818d5f
--- /dev/null
+++ b/e2etests/tests/integration/test_subscribe_command.rs
@@ -0,0 +1,206 @@
+#[allow(unused_imports)]
+use q_cli_e2e_tests::q_chat_helper;
+use std::sync::{Mutex, Once, atomic::{AtomicUsize, Ordering}};
+#[allow(dead_code)]
+static INIT: Once = Once::new();
+#[allow(dead_code)]
+static mut CHAT_SESSION: Option> = None;
+
+#[allow(dead_code)]
+pub fn get_chat_session() -> &'static Mutex {
+ unsafe {
+ INIT.call_once(|| {
+ let chat = q_chat_helper::QChatSession::new().expect("Failed to create chat session");
+ println!("β Q Chat session started");
+ CHAT_SESSION = Some(Mutex::new(chat));
+ });
+ (&raw const CHAT_SESSION).as_ref().unwrap().as_ref().unwrap()
+ }
+}
+
+#[allow(dead_code)]
+pub fn cleanup_if_last_test(test_count: &AtomicUsize, total_tests: usize) -> Result> {
+ let count = test_count.fetch_add(1, Ordering::SeqCst) + 1;
+ if count == total_tests {
+ unsafe {
+ if let Some(session) = (&raw const CHAT_SESSION).as_ref().unwrap() {
+ if let Ok(mut chat) = session.lock() {
+ chat.quit()?;
+ println!("β Test completed successfully");
+ }
+ }
+ }
+ }
+ Ok(count)
+}
+
+#[allow(dead_code)]
+static TEST_COUNT: AtomicUsize = AtomicUsize::new(0);
+
+// List of covered tests
+#[allow(dead_code)]
+const TEST_NAMES: &[&str] = &[
+ "test_subscribe_command",
+ "test_subscribe_manage_command",
+ "test_subscribe_help_command",
+ "test_subscribe_h_command",
+];
+#[allow(dead_code)]
+const TOTAL_TESTS: usize = TEST_NAMES.len();
+
+
+#[test]
+#[cfg(all(feature = "subscribe", feature = "sanity"))]
+fn test_subscribe_command() -> Result<(), Box> {
+ println!("\nπ Testing /subscribe command... | Description: Tests the /subscribe command to display Q Developer Pro subscription information and IAM Identity Center details");
+
+ let session = get_chat_session();
+ let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
+
+ let response = chat.execute_command("/subscribe")?;
+
+ println!("π Subscribe response: {} bytes", response.len());
+ println!("π FULL OUTPUT:");
+ println!("{}", response);
+ println!("π END OUTPUT");
+
+ // Verify subscription management message
+ assert!(response.contains("Q Developer Pro subscription") && response.contains("IAM Identity Center"), "Missing subscription management message");
+ println!("β Found subscription management message");
+
+ println!("β All subscribe content verified!");
+
+ // Release the lock before cleanup
+ drop(chat);
+
+ // Cleanup only if this is the last test
+ cleanup_if_last_test(&TEST_COUNT, TOTAL_TESTS)?;
+
+ Ok(())
+}
+
+#[test]
+#[cfg(all(feature = "subscribe", feature = "sanity"))]
+fn test_subscribe_manage_command() -> Result<(), Box> {
+ println!("\nπ Testing /subscribe --manage command... | Description: Tests the /subscribe --manage command to access subscription management interface for Q Developer Pro");
+
+ let session = get_chat_session();
+ let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
+
+ let response = chat.execute_command("/subscribe --manage")?;
+
+ println!("π Subscribe response: {} bytes", response.len());
+ println!("π FULL OUTPUT:");
+ println!("{}", response);
+ println!("π END OUTPUT");
+
+ // Verify subscription management message
+ assert!(response.contains("Q Developer Pro subscription") && response.contains("IAM Identity Center"), "Missing subscription management message");
+ println!("β Found subscription management message");
+
+ println!("β All subscribe content verified!");
+
+ // Release the lock before cleanup
+ drop(chat);
+
+ // Cleanup only if this is the last test
+ cleanup_if_last_test(&TEST_COUNT, TOTAL_TESTS)?;
+
+ Ok(())
+}
+
+#[test]
+#[cfg(all(feature = "subscribe", feature = "sanity"))]
+fn test_subscribe_help_command() -> Result<(), Box> {
+ println!("\nπ Testing /subscribe --help command... | Description: Tests the /subscribe --help command to display comprehensive help information for subscription management");
+
+ let session = get_chat_session();
+ let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
+
+ let response = chat.execute_command("/subscribe --help")?;
+
+ println!("π Subscribe help response: {} bytes", response.len());
+ println!("π FULL OUTPUT:");
+ println!("{}", response);
+ println!("π END OUTPUT");
+
+ // Verify description
+ assert!(response.contains("Q Developer Pro subscription"), "Missing subscription description");
+ println!("β Found subscription description");
+
+ // Verify Usage section
+ assert!(response.contains("Usage:"), "Missing Usage section");
+ assert!(response.contains("/subscribe"), "Missing /subscribe command in usage section");
+ assert!(response.contains("[OPTIONS]"), "Missing [OPTIONS] in usage section");
+ println!("β Found Usage section with /subscribe [OPTIONS]");
+
+ // Verify Options section
+ assert!(response.contains("Options:"), "Missing Options section");
+ println!("β Found Options section");
+
+ // Verify manage option
+ assert!(response.contains("--manage"), "Missing --manage option");
+ println!("β Found --manage option");
+
+ // Verify help flags
+ assert!(response.contains("-h") && response.contains("--help"), "Missing -h, --help flags");
+ println!("β Found help flags: -h, --help with Print help description");
+
+ println!("β All subscribe help content verified!");
+
+ // Release the lock before cleanup
+ drop(chat);
+
+ // Cleanup only if this is the last test
+ cleanup_if_last_test(&TEST_COUNT, TOTAL_TESTS)?;
+
+ Ok(())
+}
+
+#[test]
+#[cfg(all(feature = "subscribe", feature = "sanity"))]
+fn test_subscribe_h_command() -> Result<(), Box> {
+ println!("\nπ Testing /subscribe -h command... | Description: Tests the /subscribe -h command (short form) to display subscription help information");
+
+ let session = get_chat_session();
+ let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
+
+ let response = chat.execute_command("/subscribe -h")?;
+
+ println!("π Subscribe help response: {} bytes", response.len());
+ println!("π FULL OUTPUT:");
+ println!("{}", response);
+ println!("π END OUTPUT");
+
+ // Verify description
+ assert!(response.contains("Q Developer Pro subscription"), "Missing subscription description");
+ println!("β Found subscription description");
+
+ // Verify Usage section
+ assert!(response.contains("Usage:"), "Missing Usage section");
+ assert!(response.contains("/subscribe"), "Missing /subscribe command in usage section");
+ assert!(response.contains("[OPTIONS]"), "Missing [OPTIONS] in usage section");
+ println!("β Found Usage section with /subscribe [OPTIONS]");
+
+ // Verify Options section
+ assert!(response.contains("Options:"), "Missing Options section");
+ println!("β Found Options section");
+
+ // Verify manage option
+ assert!(response.contains("--manage"), "Missing --manage option");
+ println!("β Found --manage option");
+
+ // Verify help flags
+ assert!(response.contains("-h") && response.contains("--help"), "Missing -h, --help flags");
+ println!("β Found help flags: -h, --help with Print help description");
+
+ println!("β All subscribe help content verified!");
+
+ // Release the lock before cleanup
+ drop(chat);
+
+ // Cleanup only if this is the last test
+ cleanup_if_last_test(&TEST_COUNT, TOTAL_TESTS)?;
+
+ Ok(())
+}
\ No newline at end of file
diff --git a/e2etests/tests/mcp/mod.rs b/e2etests/tests/mcp/mod.rs
new file mode 100644
index 0000000000..2cdaa6505e
--- /dev/null
+++ b/e2etests/tests/mcp/mod.rs
@@ -0,0 +1,3 @@
+pub mod test_mcp_command_regression;
+pub mod test_mcp_command;
+pub mod test_q_mcp_subcommand;
diff --git a/e2etests/tests/mcp/test_mcp_command.rs b/e2etests/tests/mcp/test_mcp_command.rs
new file mode 100644
index 0000000000..770af280c7
--- /dev/null
+++ b/e2etests/tests/mcp/test_mcp_command.rs
@@ -0,0 +1,130 @@
+#[allow(unused_imports)]
+use q_cli_e2e_tests::q_chat_helper;
+use std::sync::{Mutex, Once, atomic::{AtomicUsize, Ordering}};
+#[allow(dead_code)]
+static INIT: Once = Once::new();
+#[allow(dead_code)]
+static mut CHAT_SESSION: Option> = None;
+
+#[allow(dead_code)]
+pub fn get_chat_session() -> &'static Mutex {
+ unsafe {
+ INIT.call_once(|| {
+ let chat = q_chat_helper::QChatSession::new().expect("Failed to create chat session");
+ println!("β Q Chat session started");
+ CHAT_SESSION = Some(Mutex::new(chat));
+ });
+ (&raw const CHAT_SESSION).as_ref().unwrap().as_ref().unwrap()
+ }
+}
+
+#[allow(dead_code)]
+pub fn cleanup_if_last_test(test_count: &AtomicUsize, total_tests: usize) -> Result> {
+ let count = test_count.fetch_add(1, Ordering::SeqCst) + 1;
+ if count == total_tests {
+ unsafe {
+ if let Some(session) = (&raw const CHAT_SESSION).as_ref().unwrap() {
+ if let Ok(mut chat) = session.lock() {
+ chat.quit()?;
+ println!("β Test completed successfully");
+ }
+ }
+ }
+ }
+ Ok(count)
+}
+#[allow(dead_code)]
+static TEST_COUNT: AtomicUsize = AtomicUsize::new(0);
+
+// List of covered tests
+#[allow(dead_code)]
+const TEST_NAMES: &[&str] = &[
+ "test_mcp_help_command",
+ "test_mcp_loading_command",
+];
+#[allow(dead_code)]
+const TOTAL_TESTS: usize = TEST_NAMES.len();
+
+#[test]
+#[cfg(all(feature = "mcp", feature = "sanity"))]
+fn test_mcp_help_command() -> Result<(), Box> {
+ println!("\nπ Testing /mcp --help command... | Description: Tests the /mcp --help command to display help information for MCP server management functionality");
+
+ let session = get_chat_session();
+ let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
+
+ let response = chat.execute_command("/mcp --help")?;
+
+ println!("π MCP help response: {} bytes", response.len());
+ println!("π FULL OUTPUT:");
+ println!("{}", response);
+ println!("π END OUTPUT");
+
+ // Verify description
+ assert!(response.contains("See mcp server loaded"), "Missing mcp server description");
+ println!("β Found mcp server description");
+
+ // Verify Usage section
+ assert!(response.contains("Usage"), "Missing Usage section");
+ assert!(response.contains("/mcp"), "Missing /mcp command in usage section");
+ println!("β Found Usage section with /mcp command");
+
+ // Verify Options section
+ assert!(response.contains("Options"), "Missing Options section");
+ println!("β Found Options section");
+
+ // Verify help flags
+ assert!(response.contains("-h") && response.contains("--help") && response.contains("Print help"), "Missing -h, --help flags");
+ println!("β Found help flags: -h, --help with Print help description");
+
+ println!("β All mcp help content verified!");
+
+ // Release the lock before cleanup
+ drop(chat);
+
+ // Cleanup only if this is the last test
+ cleanup_if_last_test(&TEST_COUNT, TOTAL_TESTS)?;
+
+ Ok(())
+}
+
+#[test]
+#[cfg(all(feature = "mcp", feature = "sanity"))]
+fn test_mcp_loading_command() -> Result<(), Box> {
+ println!("\nπ Testing /mcp command... | Description: Tests the /mcp command to display MCP server loading status and information");
+
+ let session = get_chat_session();
+ let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
+
+ let response = chat.execute_command("/mcp")?;
+
+ println!("π MCP loading response: {} bytes", response.len());
+ println!("π FULL OUTPUT:");
+ println!("{}", response);
+ println!("π END OUTPUT");
+
+ // Check MCP status - either loaded or loading
+ if response.contains("loaded in") {
+ assert!(response.contains(" s"), "Missing seconds indicator for loading time");
+ println!("β Found MCPs loaded with timing");
+
+ // Count number of MCPs loaded
+ let mcp_count = response.matches("β").count();
+ println!("β Found {} MCP(s) loaded", mcp_count);
+ } else if response.contains("loading") {
+ println!("β MCPs are still loading");
+ } else {
+ println!("βΉοΈ MCP status unclear - may be in different state");
+ }
+
+ println!("β All MCP loading content verified!");
+
+ // Release the lock before cleanup
+ drop(chat);
+
+ // Cleanup only if this is the last test
+ cleanup_if_last_test(&TEST_COUNT, TOTAL_TESTS)?;
+
+ Ok(())
+}
+
diff --git a/e2etests/tests/test_mcp_command.rs b/e2etests/tests/mcp/test_mcp_command_regression.rs
similarity index 51%
rename from e2etests/tests/test_mcp_command.rs
rename to e2etests/tests/mcp/test_mcp_command_regression.rs
index 31f34b7f32..c4c53cbadb 100644
--- a/e2etests/tests/test_mcp_command.rs
+++ b/e2etests/tests/mcp/test_mcp_command_regression.rs
@@ -1,116 +1,67 @@
-use q_cli_e2e_tests::{get_chat_session, cleanup_if_last_test};
-use std::sync::atomic::{AtomicUsize, Ordering};
+#[allow(unused_imports)]
+use q_cli_e2e_tests::q_chat_helper;
+use std::sync::{Mutex, Once, atomic::{AtomicUsize, Ordering}};
+#[allow(dead_code)]
+static INIT: Once = Once::new();
+#[allow(dead_code)]
+static mut CHAT_SESSION: Option> = None;
+#[allow(dead_code)]
+pub fn get_chat_session() -> &'static Mutex {
+ unsafe {
+ INIT.call_once(|| {
+ let chat = q_chat_helper::QChatSession::new().expect("Failed to create chat session");
+ println!("β Q Chat session started");
+ CHAT_SESSION = Some(Mutex::new(chat));
+ });
+ (&raw const CHAT_SESSION).as_ref().unwrap().as_ref().unwrap()
+ }
+}
+
+#[allow(dead_code)]
+pub fn cleanup_if_last_test(test_count: &AtomicUsize, total_tests: usize) -> Result> {
+ let count = test_count.fetch_add(1, Ordering::SeqCst) + 1;
+ if count == total_tests {
+ unsafe {
+ if let Some(session) = (&raw const CHAT_SESSION).as_ref().unwrap() {
+ if let Ok(mut chat) = session.lock() {
+ chat.quit()?;
+ println!("β Test completed successfully");
+ }
+ }
+ }
+ }
+ Ok(count)
+}
+#[allow(dead_code)]
static TEST_COUNT: AtomicUsize = AtomicUsize::new(0);
// List of covered tests
+#[allow(dead_code)]
const TEST_NAMES: &[&str] = &[
- "test_mcp_help_command",
- "test_mcp_loading_command",
"test_mcp_remove_help_command",
- "test_q_mcp_add_help_command",
- "test_q_mcp_help_command",
- "test_q_mcp_import_help_command",
- "test_q_mcp_list_command",
- "test_q_mcp_list_help_command",
- "test_q_mcp_status_help_command",
- "test_add_and_remove_mcp_command"
+ "test_mcp_add_help_command",
+ "test_mcp_help_command",
+ "test_mcp_import_help_command",
+ "test_mcp_list_command",
+ "test_mcp_list_help_command",
+ "test_mcp_status_help_command",
+ "test_add_and_remove_mcp_command",
+ "test_mcp_status_command"
];
+#[allow(dead_code)]
const TOTAL_TESTS: usize = TEST_NAMES.len();
#[test]
-#[cfg(feature = "mcp")]
-fn test_mcp_help_command() -> Result<(), Box> {
- println!("π Testing /mcp --help command...");
-
- let session = get_chat_session();
- let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
-
- let response = chat.execute_command("/mcp --help")?;
-
- println!("π MCP help response: {} bytes", response.len());
- println!("π FULL OUTPUT:");
- println!("{}", response);
- println!("π END OUTPUT");
-
- // Verify description
- assert!(response.contains("See mcp server loaded"), "Missing mcp server description");
- println!("β Found mcp server description");
-
- // Verify Usage section
- assert!(response.contains("Usage"), "Missing Usage section");
- assert!(response.contains("/mcp"), "Missing /mcp command in usage section");
- println!("β Found Usage section with /mcp command");
-
- // Verify Options section
- assert!(response.contains("Options"), "Missing Options section");
- println!("β Found Options section");
-
- // Verify help flags
- assert!(response.contains("-h") && response.contains("--help") && response.contains("Print help"), "Missing -h, --help flags");
- println!("β Found help flags: -h, --help with Print help description");
-
- println!("β All mcp help content verified!");
-
- // Release the lock before cleanup
- drop(chat);
-
- // Cleanup only if this is the last test
- cleanup_if_last_test(&TEST_COUNT, TOTAL_TESTS)?;
-
- Ok(())
-}
-
-#[test]
-#[cfg(feature = "mcp")]
-fn test_mcp_loading_command() -> Result<(), Box> {
- println!("π Testing MCP loading...");
-
- let session = get_chat_session();
- let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
-
- let response = chat.execute_command("/mcp")?;
-
- println!("π MCP loading response: {} bytes", response.len());
- println!("π FULL OUTPUT:");
- println!("{}", response);
- println!("π END OUTPUT");
-
- // Check MCP status - either loaded or loading
- if response.contains("loaded in") {
- assert!(response.contains(" s"), "Missing seconds indicator for loading time");
- println!("β Found MCPs loaded with timing");
-
- // Count number of MCPs loaded
- let mcp_count = response.matches("β").count();
- println!("β Found {} MCP(s) loaded", mcp_count);
- } else if response.contains("loading") {
- println!("β MCPs are still loading");
- } else {
- println!("βΉοΈ MCP status unclear - may be in different state");
- }
-
- println!("β All MCP loading content verified!");
-
- // Release the lock before cleanup
- drop(chat);
-
- // Cleanup only if this is the last test
- cleanup_if_last_test(&TEST_COUNT, TOTAL_TESTS)?;
-
- Ok(())
-}
-
-#[test]
-#[cfg(feature = "mcp")]
+#[cfg(all(feature = "mcp", feature = "regression"))]
fn test_mcp_remove_help_command() -> Result<(), Box> {
- println!("π Testing q mcp remove --help command...");
+ println!("\nπ Testing q mcp remove --help command... | Description: Tests the q mcp remove --help command to display help information for removing MCP servers");
let session = get_chat_session();
let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
// Execute q mcp remove --help command
- let help_response = chat.execute_command("q mcp remove --help")?;
+ let help_response = chat.execute_command("execute below bash command q mcp remove --help")?;
println!("π MCP remove help response: {} bytes", help_response.len());
println!("π HELP RESPONSE:");
@@ -118,8 +69,8 @@ fn test_mcp_remove_help_command() -> Result<(), Box> {
println!("π END HELP RESPONSE");
// Verify tool execution prompt appears
- assert!(help_response.contains("π οΈ Using tool: execute_bash"), "Missing tool execution indicator");
- assert!(help_response.contains("Allow this action?") && help_response.contains("to trust (always allow) this tool for the session."), "Missing permission prompt");
+ assert!(help_response.contains("Using tool"), "Missing tool execution indicator");
+ assert!(help_response.contains("Allow this action?"), "Missing permission prompt");
println!("β Found tool execution permission prompt");
// Allow the tool execution
@@ -131,13 +82,12 @@ fn test_mcp_remove_help_command() -> Result<(), Box> {
println!("π END ALLOW RESPONSE");
// Verify complete help content in final response
- assert!(allow_response.contains("Usage: qchat mcp remove"), "Missing usage information");
+ assert!(allow_response.contains("Usage") && allow_response.contains("qchat mcp remove"), "Missing usage information");
assert!(allow_response.contains("Options"), "Missing option information");
assert!(allow_response.contains("--name "), "Missing --name option");
assert!(allow_response.contains("--scope "), "Missing --scope option");
assert!(allow_response.contains("--agent "), "Missing --agent option");
assert!(allow_response.contains("-h, --help"), "Missing help option");
- assert!(allow_response.contains("Completed in"), "Missing completion indicator");
println!("β Found all expected MCP remove help content and completion");
// Release the lock before cleanup
@@ -150,16 +100,16 @@ fn test_mcp_remove_help_command() -> Result<(), Box> {
}
#[test]
-#[cfg(feature = "mcp")]
-fn test_q_mcp_add_help_command() -> Result<(), Box> {
- println!("π Testing q mcp add --help command...");
+#[cfg(all(feature = "mcp", feature = "regression"))]
+fn test_mcp_add_help_command() -> Result<(), Box> {
+ println!("\nπ Testing q mcp add --help command... | Description: Tests the q mcp add --help command to display help information for adding new MCP servers");
let session = get_chat_session();
let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
// Execute mcp add --help command
- println!("π Executing command: 'q mcp add --help'");
- let response = chat.execute_command("q mcp add --help")?;
+ println!("\nπ Executing command: 'q mcp add --help'");
+ let response = chat.execute_command("execute below bash command q mcp add --help")?;
println!("π Restart response: {} bytes", response.len());
println!("π RESTART RESPONSE:");
@@ -168,12 +118,12 @@ fn test_q_mcp_add_help_command() -> Result<(), Box> {
// Verify tool execution details
assert!(response.contains("q mcp add --help"), "Missing command execution description");
- assert!(response.contains("Purpose:"), "Missing purpose description");
+ assert!(response.contains("Purpose"), "Missing purpose description");
println!("β Found tool execution details");
// Verify tool execution prompt appears
- assert!(response.contains("π οΈ Using tool: execute_bash"), "Missing tool execution indicator");
- assert!(response.contains("Allow this action?") && response.contains("to trust (always allow) this tool for the session."), "Missing permission prompt");
+ assert!(response.contains("Using tool"), "Missing tool execution indicator");
+ assert!(response.contains("Allow this action?"), "Missing permission prompt");
println!("β Found tool execution permission prompt");
// Allow the tool execution
@@ -185,18 +135,13 @@ fn test_q_mcp_add_help_command() -> Result<(), Box> {
println!("π END ALLOW RESPONSE");
// Verify mcp add help output
- assert!(allow_response.contains("Usage: qchat mcp add"), "Missing usage information");
+ assert!(allow_response.contains("Usage") && allow_response.contains("qchat mcp add"), "Missing usage information");
assert!(allow_response.contains("Options"), "Missing Options");
assert!(allow_response.contains("--name "), "Missing --name option");
assert!(allow_response.contains("--command "), "Missing --command option");
assert!(allow_response.contains("--scope "), "Missing --scope option");
- assert!(allow_response.contains("--args "), "Missing --args option");
assert!(allow_response.contains("--agent "), "Missing --agent option");
- assert!(allow_response.contains("--env "), "Missing --env option");
- assert!(allow_response.contains("--timeout "), "Missing --timeout option");
- assert!(allow_response.contains("--disabled"), "Missing --disabled option");
assert!(allow_response.contains("--force"), "Missing --force option");
- assert!(allow_response.contains("--verbose"), "Missing --verbose option");
assert!(allow_response.contains("--help"), "Missing --help option");
assert!(allow_response.contains("Completed in"), "Missing completion indicator");
assert!(allow_response.contains("Required"), "Missing Requried indicator");
@@ -213,15 +158,15 @@ fn test_q_mcp_add_help_command() -> Result<(), Box> {
}
#[test]
-#[cfg(feature = "mcp")]
-fn test_q_mcp_help_command() -> Result<(), Box> {
- println!("π Testing q mcp --help command...");
+#[cfg(all(feature = "mcp", feature = "regression"))]
+fn test_mcp_help_command() -> Result<(), Box> {
+ println!("\nπ Testing q mcp --help command... | Description: Tests the q mcp --help command to display comprehensive MCP management help including all subcommands");
let session = get_chat_session();
let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
// Execute q mcp --help command
- let help_response = chat.execute_command("q mcp --help")?;
+ let help_response = chat.execute_command("execute below bash command q mcp --help")?;
println!("π MCP help response: {} bytes", help_response.len());
println!("π HELP RESPONSE:");
@@ -229,8 +174,8 @@ fn test_q_mcp_help_command() -> Result<(), Box> {
println!("π END HELP RESPONSE");
// Verify tool execution prompt appears
- assert!(help_response.contains("π οΈ Using tool: execute_bash"), "Missing tool execution indicator");
- assert!(help_response.contains("Allow this action?") && help_response.contains("to trust (always allow) this tool for the session."), "Missing permission prompt");
+ assert!(help_response.contains("Using tool"), "Missing tool execution indicator");
+ assert!(help_response.contains("Allow this action?"), "Missing permission prompt");
println!("β Found tool execution permission prompt");
// Allow the tool execution
@@ -243,8 +188,8 @@ fn test_q_mcp_help_command() -> Result<(), Box> {
// Verify complete help content
assert!(allow_response.contains("Model Context Protocol (MCP)"), "Missing MCP description");
- assert!(allow_response.contains("Usage: qchat mcp"), "Missing usage information");
- assert!(allow_response.contains("Commands:"), "Missing Commands section");
+ assert!(allow_response.contains("Usage") && allow_response.contains("qchat mcp"), "Missing usage information");
+ assert!(allow_response.contains("Commands"), "Missing Commands section");
// Verify command descriptions
assert!(allow_response.contains("add"), "Missing add command description");
@@ -258,7 +203,6 @@ fn test_q_mcp_help_command() -> Result<(), Box> {
assert!(allow_response.contains("Options"), "Missing Options section");
assert!(allow_response.contains("-v, --verbose"), "Missing verbose option");
assert!(allow_response.contains("-h, --help"), "Missing help option");
- assert!(allow_response.contains("Completed in"), "Missing completion indicator");
println!("β Found all expected MCP help content and completion");
// Release the lock before cleanup
@@ -271,16 +215,16 @@ fn test_q_mcp_help_command() -> Result<(), Box> {
}
#[test]
-#[cfg(feature = "mcp")]
-fn test_q_mcp_import_help_command() -> Result<(), Box> {
- println!("π Testing q mcp import --help command...");
+#[cfg(all(feature = "mcp", feature = "regression"))]
+fn test_mcp_import_help_command() -> Result<(), Box> {
+ println!("\nπ Testing q mcp import --help command... | Description: Tests the q mcp import --help command to display help information for importing MCP server configurations");
let session = get_chat_session();
let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
// Execute mcp import --help command
- println!("π Executing command: 'q mcp import --help'");
- let response = chat.execute_command("q mcp import --help")?;
+ println!("\nπ Executing command: 'q mcp import --help'");
+ let response = chat.execute_command("execute below bash command q mcp import --help")?;
println!("π Restart response: {} bytes", response.len());
println!("π RESTART RESPONSE:");
@@ -289,12 +233,12 @@ fn test_q_mcp_import_help_command() -> Result<(), Box> {
// Verify tool execution details
assert!(response.contains("q mcp import --help"), "Missing command execution description");
- assert!(response.contains("Purpose:"), "Missing purpose description");
+ assert!(response.contains("Purpose"), "Missing purpose description");
println!("β Found tool execution details");
// Verify tool execution prompt appears
- assert!(response.contains("π οΈ Using tool: execute_bash"), "Missing tool execution indicator");
- assert!(response.contains("Allow this action?") && response.contains("to trust (always allow) this tool for the session."), "Missing permission prompt");
+ assert!(response.contains("Using tool"), "Missing tool execution indicator");
+ assert!(response.contains("Allow this action?"), "Missing permission prompt");
println!("β Found tool execution permission prompt");
// Allow the tool execution
@@ -321,10 +265,6 @@ fn test_q_mcp_import_help_command() -> Result<(), Box> {
assert!(allow_response.contains("-h, --help"), "Missing --help option");
println!("β Found all options with descriptions");
- // Verify completion indicator
- assert!(allow_response.contains("Completed in") && allow_response.contains("s"), "Missing completion time indicator");
- println!("β Found completion indicator");
-
println!("β All q mcp import --help content verified successfully");
// Release the lock before cleanup
@@ -337,14 +277,14 @@ fn test_q_mcp_import_help_command() -> Result<(), Box> {
}
#[test]
-#[cfg(feature = "mcp")]
-fn test_q_mcp_list_command() -> Result<(), Box> {
- println!("π Testing q mcp list command...");
+#[cfg(all(feature = "mcp", feature = "regression"))]
+fn test_mcp_list_command() -> Result<(), Box> {
+ println!("\nπ Testing q mcp list command... | Description: Tests the q mcp list command to display all configured MCP servers and their status");
let session = get_chat_session();
let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
- let response = chat.execute_command("q mcp list")?;
+ let response = chat.execute_command("execute below bash command q mcp list")?;
println!("π MCP list response: {} bytes", response.len());
println!("π FULL OUTPUT:");
@@ -352,9 +292,9 @@ fn test_q_mcp_list_command() -> Result<(), Box> {
println!("π END OUTPUT");
// Verify tool execution prompt
- assert!(response.contains("Using tool:"), "Missing tool execution indicator");
+ assert!(response.contains("Using tool"), "Missing tool execution indicator");
assert!(response.contains("q mcp list"), "Missing command in tool execution");
- assert!(response.contains("Allow this action?") && response.contains("to trust (always allow) this tool for the session."), "Missing permission prompt");
+ assert!(response.contains("Allow this action?"), "Missing permission prompt");
println!("β Found tool execution permission prompt");
// Allow the tool execution
@@ -368,8 +308,6 @@ fn test_q_mcp_list_command() -> Result<(), Box> {
// Verify MCP server listing
assert!(allow_response.contains("q_cli_default"), "Missing q_cli_default server");
- assert!(allow_response.contains("default"), "Missing default tag");
- assert!(allow_response.contains("global"), "Missing global tag");
println!("β Found MCP server listing with servers and completion");
// Release the lock before cleanup
@@ -382,14 +320,14 @@ fn test_q_mcp_list_command() -> Result<(), Box> {
}
#[test]
-#[cfg(feature = "mcp")]
-fn test_q_mcp_list_help_command() -> Result<(), Box> {
- println!("π Testing q mcp list --help command...");
+#[cfg(all(feature = "mcp", feature = "regression"))]
+fn test_mcp_list_help_command() -> Result<(), Box> {
+ println!("\nπ Testing q mcp list --help command... | Description: Tests the q mcp list --help command to display help information for listing MCP servers");
let session = get_chat_session();
let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
- let response = chat.execute_command("q mcp list --help")?;
+ let response = chat.execute_command("execute below bash command q mcp list --help")?;
println!("π MCP list help response: {} bytes", response.len());
println!("π FULL OUTPUT:");
@@ -397,9 +335,9 @@ fn test_q_mcp_list_help_command() -> Result<(), Box> {
println!("π END OUTPUT");
// Verify tool execution prompt
- assert!(response.contains("Using tool:"), "Missing tool execution indicator");
+ assert!(response.contains("Using tool"), "Missing tool execution indicator");
assert!(response.contains("q mcp list --help"), "Missing command in tool execution");
- assert!(response.contains("Allow this action?") && response.contains("to trust (always allow) this tool for the session."), "Missing permission prompt");
+ assert!(response.contains("Allow this action?"), "Missing permission prompt");
println!("β Found tool execution permission prompt");
// Allow the tool execution
@@ -421,10 +359,6 @@ fn test_q_mcp_list_help_command() -> Result<(), Box> {
assert!(allow_response.contains("Options"), "Missing Options section");
assert!(allow_response.contains("-v") && allow_response.contains("--verbose"), "Missing verbose option");
assert!(allow_response.contains("-h") && allow_response.contains("--help"), "Missing help option");
-
-
- assert!(allow_response.contains("Completed in"), "Missing completion indicator");
- println!("β Found all MCP list help content with explanations and completion");
// Release the lock before cleanup
drop(chat);
@@ -436,16 +370,16 @@ fn test_q_mcp_list_help_command() -> Result<(), Box> {
}
#[test]
-#[cfg(feature = "mcp")]
-fn test_q_mcp_status_help_command() -> Result<(), Box> {
- println!("π Testing q mcp status --help command...");
+#[cfg(all(feature = "mcp", feature = "regression"))]
+fn test_mcp_status_help_command() -> Result<(), Box> {
+ println!("\nπ Testing q mcp status --help command... | Description: Tests the q mcp status --help command to display help information for checking MCP server status");
let session = get_chat_session();
let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
// Execute mcp status --help command
- println!("π Executing command: 'q mcp status --help'");
- let response = chat.execute_command("q mcp status --help")?;
+ println!("\nπ Executing command: 'q mcp status --help'");
+ let response = chat.execute_command("execute below bash command q mcp status --help")?;
println!("π Restart response: {} bytes", response.len());
println!("π RESTART RESPONSE:");
@@ -453,12 +387,12 @@ fn test_q_mcp_status_help_command() -> Result<(), Box> {
println!("π END RESTART RESPONSE");
// Verify tool execution details
- assert!(response.contains("Purpose:"), "Missing purpose description");
+ assert!(response.contains("Purpose"), "Missing purpose description");
println!("β Found tool execution details");
// Verify tool execution prompt appears
- assert!(response.contains("π οΈ Using tool: execute_bash"), "Missing tool execution indicator");
- assert!(response.contains("Allow this action?") && response.contains("to trust (always allow) this tool for the session."), "Missing permission prompt");
+ assert!(response.contains("Using tool"), "Missing tool execution indicator");
+ assert!(response.contains("Allow this action?"), "Missing permission prompt");
println!("β Found tool execution permission prompt");
// Allow the tool execution
@@ -470,20 +404,16 @@ fn test_q_mcp_status_help_command() -> Result<(), Box> {
println!("π END ALLOW RESPONSE");
// Verify usage line
- assert!(allow_response.contains("Usage: qchat mcp status [OPTIONS] --name "), "Missing complete usage line");
+ assert!(allow_response.contains("Usage") && allow_response.contains("qchat mcp status [OPTIONS] --name "), "Missing complete usage line");
println!("β Found usage information");
// Verify Options section
assert!(allow_response.contains("Options"), "Missing Options section");
assert!(allow_response.contains("--name "), "Missing --name option");
- assert!(allow_response.contains("-v, --verbose...") , "Missing --verbose option");
+ assert!(allow_response.contains("-v, --verbose") , "Missing --verbose option");
assert!(allow_response.contains("-h, --help"), "Missing --help option");
println!("β Found all options with descriptions");
- // Verify completion indicator
- assert!(allow_response.contains("Completed in") && allow_response.contains("s"), "Missing completion time indicator");
- println!("β Found completion indicator");
-
println!("β All q mcp status --help content verified successfully");
// Release the lock before cleanup
@@ -496,24 +426,66 @@ fn test_q_mcp_status_help_command() -> Result<(), Box> {
}
#[test]
-#[cfg(feature = "mcp")]
+#[cfg(all(feature = "mcp", feature = "regression"))]
fn test_add_and_remove_mcp_command() -> Result<(), Box> {
- println!("π Testing q mcp add command...");
-
+ println!("\nπ Testing q mcp add command... | Description: Tests the q mcp add and q mcp remove subcommands to add and remove MCP servers");
+
// First install uv dependency before starting Q Chat
- println!("π Installing uv dependency...");
+ println!("\nπ Installing uv dependency...");
+
std::process::Command::new("pip3")
.args(["install", "uv", "--break-system-packages"])
.output()
.expect("Failed to install uv");
- println!("β uv dependency installed");
+ println!("β uv dependency installed");
+
let session = get_chat_session();
let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
-
- // Execute mcp add command
- println!("π Executing command: 'q mcp add --name aws-documentation --command uvx --args awslabs.aws-documentation-mcp-server@latest'");
- let response = chat.execute_command("q mcp add --name aws-documentation --command uvx --args awslabs.aws-documentation-mcp-server@latest")?;
+
+ // First check if MCP already exists using q mcp list
+ println!("\nπ Checking if aws-documentation MCP already exists...");
+ let list_response = chat.execute_command("execute below bash command q mcp list")?;
+
+ println!("π List response: {} bytes", list_response.len());
+ println!("π LIST RESPONSE:");
+ println!("{}", list_response);
+ println!("π END LIST RESPONSE");
+
+ // Allow the list command
+ let list_allow_response = chat.execute_command("y")?;
+ println!("π List allow response: {} bytes", list_allow_response.len());
+ println!("π LIST ALLOW RESPONSE:");
+ println!("{}", list_allow_response);
+ println!("π END LIST ALLOW RESPONSE");
+
+ // Check if aws-documentation exists in the list
+ if list_allow_response.contains("aws-documentation") {
+ println!("\nπ aws-documentation MCP already exists, removing it first...");
+
+ let remove_response = chat.execute_command("execute below bash command q mcp remove --name aws-documentation")?;
+ println!("π Remove response: {} bytes", remove_response.len());
+ println!("π REMOVE RESPONSE:");
+ println!("{}", remove_response);
+ println!("π END REMOVE RESPONSE");
+
+ // Allow the remove command
+ let remove_allow_response = chat.execute_command("y")?;
+ println!("π Remove allow response: {} bytes", remove_allow_response.len());
+ println!("π REMOVE ALLOW RESPONSE:");
+ println!("{}", remove_allow_response);
+ println!("π END REMOVE ALLOW RESPONSE");
+
+ // Verify successful removal
+ assert!(remove_allow_response.contains("Removed") && remove_allow_response.contains("'aws-documentation'"), "Missing removal success message");
+ println!("β Successfully removed existing aws-documentation MCP");
+ } else {
+ println!("β aws-documentation MCP does not exist, proceeding with add");
+ }
+
+ // Now add the MCP server
+ println!("\nπ Executing command: 'q mcp add --name aws-documentation --command uvx --args awslabs.aws-documentation-mcp-server@latest'");
+ let response = chat.execute_command("execute below bash command q mcp add --name aws-documentation --command uvx --args awslabs.aws-documentation-mcp-server@latest")?;
println!("π Response: {} bytes", response.len());
println!("π RESPONSE:");
@@ -521,78 +493,157 @@ fn test_add_and_remove_mcp_command() -> Result<(), Box> {
println!("π END RESPONSE");
// Verify tool execution details
- assert!(response.contains("Using tool:"), "Missing using tool indicator");
assert!(response.contains("q mcp add --name aws-documentation --command uvx --args awslabs.aws-documentation-mcp-server@latest"), "Missing full command");
- assert!(response.contains("Purpose:"), "Missing purpose description");
- println!("β Found tool execution details");
-
- // Verify tool execution prompt appears
- assert!(response.contains("π οΈ Using tool: execute_bash"), "Missing tool execution indicator");
- assert!(response.contains("Allow this action?") && response.contains("to trust (always allow) this tool for the session."), "Missing permission prompt");
+ assert!(response.contains("Using tool"), "Missing tool execution indicator");
+ assert!(response.contains("Allow this action?"), "Missing permission prompt");
println!("β Found tool execution permission prompt");
// Allow the tool execution
let allow_response = chat.execute_command("y")?;
-
println!("π Allow response: {} bytes", allow_response.len());
println!("π ALLOW RESPONSE:");
println!("{}", allow_response);
println!("π END ALLOW RESPONSE");
// Verify successful addition
- assert!(allow_response.contains("Added MCP server") && allow_response.contains("'aws-documentation'"), "Missing success message");
+ assert!(allow_response.contains("Added") && allow_response.contains("'aws-documentation'"), "Missing success message");
assert!(allow_response.contains("/Users/") && allow_response.contains("/.aws/amazonq/mcp.json"), "Missing config file path");
println!("β Found successful addition message");
- // Verify completion indicator
- assert!(allow_response.contains("Completed in") && allow_response.contains("s"), "Missing completion time indicator");
- println!("β Found completion indicator");
-
- println!("β All q mcp add command execution verified successfully");
-
// Now test removing the MCP server
- println!("π Executing remove command: 'q mcp remove --name aws-documentation'");
- let remove_response = chat.execute_command("q mcp remove --name aws-documentation")?;
-
+ println!("\nπ Executing remove command: 'q mcp remove --name aws-documentation'");
+ let remove_response = chat.execute_command("execute below bash command q mcp remove --name aws-documentation")?;
println!("π Remove response: {} bytes", remove_response.len());
println!("π REMOVE RESPONSE:");
println!("{}", remove_response);
println!("π END REMOVE RESPONSE");
// Verify remove tool execution details
- assert!(response.contains("Using tool:"), "Missing using tool indicator");
+ assert!(response.contains("Using tool"), "Missing using tool indicator");
assert!(remove_response.contains("q mcp remove --name aws-documentation"), "Missing full remove command");
- println!("β Found remove tool execution details");
-
- // Verify remove tool execution prompt
- assert!(remove_response.contains("π οΈ Using tool: execute_bash"), "Missing remove tool execution indicator");
assert!(remove_response.contains("Allow this action?"), "Missing remove permission prompt");
println!("β Found remove tool execution permission prompt");
// Allow the remove tool execution
let remove_allow_response = chat.execute_command("y")?;
-
println!("π Remove allow response: {} bytes", remove_allow_response.len());
println!("π REMOVE ALLOW RESPONSE:");
println!("{}", remove_allow_response);
println!("π END REMOVE ALLOW RESPONSE");
// Verify successful removal
- assert!(remove_allow_response.contains("Removed MCP server") && remove_allow_response.contains("'aws-documentation'"), "Missing removal success message");
+ assert!(remove_allow_response.contains("Removed") && remove_allow_response.contains("'aws-documentation'"), "Missing removal success message");
assert!(remove_allow_response.contains("/Users/") && remove_allow_response.contains("/.aws/amazonq/mcp.json"), "Missing config file path in removal");
println!("β Found successful removal message");
- // Verify remove completion indicator
- assert!(remove_allow_response.contains("Completed in") && remove_allow_response.contains("s"), "Missing remove completion time indicator");
- println!("β Found remove completion indicator");
+ // Release the lock before cleanup
+ drop(chat);
+
+ // Cleanup only if this is the last test
+ cleanup_if_last_test(&TEST_COUNT, TOTAL_TESTS)?;
+ Ok(())
+}
+
+#[test]
+#[cfg(all(feature = "mcp", feature = "regression"))]
+fn test_mcp_status_command() -> Result<(), Box> {
+ println!("\nπ Testing q mcp status --name command... | Description: Tests the q mcp status command with server name to display detailed status information for a specific MCP server");
+
+ // First install uv dependency before starting Q Chat
+ println!("\nπ Installing uv dependency...");
+
+ std::process::Command::new("pip3")
+ .args(["install", "uv", "--break-system-packages"])
+ .output()
+ .expect("Failed to install uv");
+
+ println!("β uv dependency installed");
+
+ let session = get_chat_session();
+ let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
+
+ // Execute mcp add command
+ println!("\nπ Executing command: 'q mcp add --name aws-documentation --command uvx --args awslabs.aws-documentation-mcp-server@latest'");
+ let response = chat.execute_command("execute below bash command q mcp add --name aws-documentation --command uvx --args awslabs.aws-documentation-mcp-server@latest")?;
- println!("β All q mcp remove command execution verified successfully");
+ println!("π Response: {} bytes", response.len());
+ println!("π RESPONSE:");
+ println!("{}", response);
+ println!("π END RESPONSE");
+
+ // Verify tool execution details
+ assert!(response.contains("q mcp add --name aws-documentation --command uvx --args awslabs.aws-documentation-mcp-server@latest"), "Missing full command");
+ assert!(response.contains("Using tool"), "Missing tool execution indicator");
+ println!("β Found tool execution permission prompt");
+
+ // Allow the tool execution
+ let allow_response = chat.execute_command("y")?;
+ println!("π Allow response: {} bytes", allow_response.len());
+ println!("π ALLOW RESPONSE:");
+ println!("{}", allow_response);
+ println!("π END ALLOW RESPONSE");
+
+ // Verify successful addition
+ assert!(allow_response.contains("Added") && allow_response.contains("'aws-documentation'"), "Missing success message");
+ println!("β Found successful addition message");
+
+ // Allow the tool execution
+ let response = chat.execute_command("execute below bash command q mcp status --name aws-documentation")?;
+ println!("π Allow response: {} bytes", response.len());
+ println!("π ALLOW RESPONSE:");
+ println!("{}", response);
+ println!("π END ALLOW RESPONSE");
+
+ // Verify tool execution details
+ assert!(response.contains("q mcp status --name aws-documentation"), "Missing full command");
+ assert!(response.contains("Using tool"), "Missing tool execution indicator");
+ println!("β Found tool execution permission prompt");
+
+ // Allow the tool execution
+ let show_response = chat.execute_command("y")?;
+ println!("π Allow response: {} bytes", show_response.len());
+ println!("π ALLOW RESPONSE:");
+ println!("{}", show_response);
+ println!("π END ALLOW RESPONSE");
+
+
+ // Verify successful status retrieval
+ assert!(show_response.contains("Scope"), "Missing Scope");
+ assert!(show_response.contains("Agent"), "Missing Agent");
+ assert!(show_response.contains("Command"), "Missing Command");
+ assert!(show_response.contains("Disabled"), "Missing Disabled");
+ assert!(show_response.contains("Env Vars"), "Missing Env Vars");
+
+ // Now test removing the MCP server
+ println!("\nπ Executing remove command: 'q mcp remove --name aws-documentation'");
+ let remove_response = chat.execute_command("execute below bash command q mcp remove --name aws-documentation")?;
+ println!("π Remove response: {} bytes", remove_response.len());
+ println!("π REMOVE RESPONSE:");
+ println!("{}", remove_response);
+ println!("π END REMOVE RESPONSE");
+
+ // Verify remove tool execution details
+ assert!(response.contains("Using tool"), "Missing using tool indicator");
+ assert!(remove_response.contains("q mcp remove --name aws-documentation"), "Missing full remove command");
+ assert!(remove_response.contains("Allow this action?"), "Missing remove permission prompt");
+ println!("β Found remove tool execution permission prompt");
+
+ // Allow the remove tool execution
+ let remove_allow_response = chat.execute_command("y")?;
+ println!("π Remove allow response: {} bytes", remove_allow_response.len());
+ println!("π REMOVE ALLOW RESPONSE:");
+ println!("{}", remove_allow_response);
+ println!("π END REMOVE ALLOW RESPONSE");
+
+ // Verify successful removal
+ assert!(remove_allow_response.contains("Removed") && remove_allow_response.contains("'aws-documentation'"), "Missing removal success message");
+ assert!(remove_allow_response.contains("/Users/") && remove_allow_response.contains("/.aws/amazonq/mcp.json"), "Missing config file path in removal");
+ println!("β Found successful removal message");
// Release the lock before cleanup
drop(chat);
// Cleanup only if this is the last test
cleanup_if_last_test(&TEST_COUNT, TOTAL_TESTS)?;
-
Ok(())
-}
\ No newline at end of file
+}
diff --git a/e2etests/tests/mcp/test_q_mcp_subcommand.rs b/e2etests/tests/mcp/test_q_mcp_subcommand.rs
new file mode 100644
index 0000000000..2f691e1728
--- /dev/null
+++ b/e2etests/tests/mcp/test_q_mcp_subcommand.rs
@@ -0,0 +1,353 @@
+#[allow(unused_imports)]
+use q_cli_e2e_tests::q_chat_helper;
+
+#[test]
+#[cfg(all(feature = "mcp", feature = "sanity"))]
+fn test_q_mcp_help_subcommand() -> Result<(), Box> {
+ println!("\nπ Testing q mcp --help subcommand... | Description: Tests the q mcp --help subcommand to display comprehensive MCP management help including all commands");
+
+ println!("\nπ Executing q [subcommand]: 'q mcp --help'");
+ let response = q_chat_helper::execute_q_subcommand("q", &["mcp", "--help"])?;
+
+ println!("π MCP help response: {} bytes", response.len());
+ println!("π FULL OUTPUT:");
+ println!("{}", response);
+ println!("π END OUTPUT");
+
+ // Verify complete help content
+ assert!(response.contains("Model Context Protocol (MCP)"), "Missing MCP description");
+ assert!(response.contains("Usage") && response.contains("qchat mcp"), "Missing usage information");
+ assert!(response.contains("Commands"), "Missing Commands section");
+
+ // Verify command descriptions
+ assert!(response.contains("add"), "Missing add command description");
+ assert!(response.contains("remove"), "Missing remove command description");
+ assert!(response.contains("list"), "Missing list command description");
+ assert!(response.contains("import"), "Missing import command description");
+ assert!(response.contains("status"), "Missing status command description");
+ assert!(response.contains("help"), "Missing help command");
+ println!("β Found all MCP commands with descriptions");
+
+ Ok(())
+}
+
+#[test]
+#[cfg(all(feature = "mcp", feature = "sanity"))]
+fn test_q_mcp_remove_help_subcommand() -> Result<(), Box> {
+ println!("\nπ Testing q mcp remove --help subcommand... | Description: Tests the q mcp remove --help subcommand to display help information for removing MCP servers");
+
+ println!("\nπ Executing q [subcommand]: 'q mcp remove --help'");
+ let response = q_chat_helper::execute_q_subcommand("q", &["mcp", "remove", "--help"])?;
+
+ println!("π MCP remove help response: {} bytes", response.len());
+ println!("π FULL OUTPUT:");
+ println!("{}", response);
+ println!("π END OUTPUT");
+
+ // Verify complete help content in final response
+ assert!(response.contains("Usage") && response.contains("qchat mcp remove"), "Missing usage information");
+ assert!(response.contains("Options"), "Missing option information");
+ assert!(response.contains("--name"), "Missing --name option");
+ assert!(response.contains("--scope"), "Missing --scope option");
+ assert!(response.contains("--agent"), "Missing --agent option");
+ assert!(response.contains("-h") && response.contains("--help"), "Missing help option");
+ println!("β Found all expected MCP remove help content and completion");
+
+ Ok(())
+}
+
+#[test]
+#[cfg(all(feature = "mcp", feature = "sanity"))]
+fn test_q_mcp_add_help_subcommand() -> Result<(), Box> {
+ println!("\nπ Testing q mcp add --help subcommand... | Description: Tests the q mcp add --help subcommand to display help information for adding new MCP servers");
+
+ println!("\nπ Executing q [subcommand]: 'q mcp add --help'");
+ let response = q_chat_helper::execute_q_subcommand("q", &["mcp", "add", "--help"])?;
+
+ println!("π Restart response: {} bytes", response.len());
+ println!("π FULL OUTPUT:");
+ println!("{}", response);
+ println!("π END OUTPUT");
+
+ // Verify mcp add help output
+ assert!(response.contains("Usage") && response.contains("qchat mcp add"), "Missing usage information");
+ assert!(response.contains("Options"), "Missing Options");
+ assert!(response.contains("--name"), "Missing --name option");
+ assert!(response.contains("--command"), "Missing --command option");
+ assert!(response.contains("--scope"), "Missing --scope option");
+ assert!(response.contains("--agent"), "Missing --agent option");
+ println!("β MCP add help subcommand executed successfully");
+
+ Ok(())
+}
+
+#[test]
+#[cfg(all(feature = "mcp", feature = "sanity"))]
+fn test_q_mcp_import_help_subcommand() -> Result<(), Box> {
+ println!("\nπ Testing q mcp import --help subcommand... | Description: Tests the q mcp import --help subcommand to display help information for importing MCP server configurations");
+
+ println!("\nπ Executing q [subcommand]: 'q mcp import --help'");
+ let response = q_chat_helper::execute_q_subcommand("q", &["mcp", "import", "--help"])?;
+
+ println!("π Restart response: {} bytes", response.len());
+ println!("π FULL OUTPUT:");
+ println!("{}", response);
+ println!("π END OUTPUT");
+
+ // Verify Options section
+ assert!(response.contains("Options"), "Missing Options section");
+ assert!(response.contains("--file"), "Missing --file option");
+ assert!(response.contains("--force"), "Missing --force option");
+ assert!(response.contains("-v") && response.contains("--verbose"), "Missing --verbose option");
+ assert!(response.contains("-h") && response.contains("--help"), "Missing --help option");
+ println!("β Found all options with descriptions");
+
+ println!("β All q mcp import --help content verified successfully");
+
+ Ok(())
+}
+
+#[test]
+#[cfg(all(feature = "mcp", feature = "sanity"))]
+fn test_q_mcp_list_subcommand() -> Result<(), Box> {
+ println!("\nπ Testing q mcp list subcommand... | Description: Tests the q mcp list subcommand to display all configured MCP servers and their status");
+
+ println!("\nπ Executing q [subcommand]: 'q mcp list'");
+ let response = q_chat_helper::execute_q_subcommand("q", &["mcp", "list"])?;
+
+ println!("π MCP list response: {} bytes", response.len());
+ println!("π FULL OUTPUT:");
+ println!("{}", response);
+ println!("π END OUTPUT");
+
+ // Verify MCP server listing
+ assert!(response.contains("q_cli_default"), "Missing q_cli_default server");
+ println!("β Found MCP server listing with servers and completion");
+
+ Ok(())
+}
+
+#[test]
+#[cfg(all(feature = "mcp", feature = "sanity"))]
+fn test_q_mcp_list_help_subcommand() -> Result<(), Box> {
+ println!("\nπ Testing q mcp list --help subcommand... | Description: Tests the q mcp list --help subcommand to display help information for listing MCP servers");
+
+ println!("\nπ Executing q [subcommand]: 'q mcp list --help'");
+ let response = q_chat_helper::execute_q_subcommand("q", &["mcp", "list", "--help"])?;
+
+ println!("π MCP list help response: {} bytes", response.len());
+ println!("π FULL OUTPUT:");
+ println!("{}", response);
+ println!("π END OUTPUT");
+
+ // Verify help content
+ assert!(response.contains("Usage"), "Missing usage format");
+
+ // Verify arguments section
+ assert!(response.contains("Arguments"), "Missing Arguments section");
+ assert!(response.contains("[SCOPE]"), "Missing scope argument");
+
+ // Verify options section
+ assert!(response.contains("Options"), "Missing Options section");
+ assert!(response.contains("-v") && response.contains("--verbose"), "Missing verbose option");
+ assert!(response.contains("-h") && response.contains("--help"), "Missing help option");
+
+ Ok(())
+}
+
+#[test]
+#[cfg(all(feature = "mcp", feature = "sanity"))]
+fn test_q_mcp_status_help_subcommand() -> Result<(), Box> {
+ println!("\nπ Testing q mcp status --help subcommand... | Description: Tests the q mcp status --help subcommand to display help information for checking MCP server status");
+
+ // Execute mcp status --help subcommand
+ println!("\nπ Executing q [subcommand]: 'q mcp status --help'");
+ let response = q_chat_helper::execute_q_subcommand("q", &["mcp", "status", "--help"])?;
+
+ println!("π Restart response: {} bytes", response.len());
+ println!("π FULL OUTPUT:");
+ println!("{}", response);
+ println!("π END OUTPUT");
+
+ // Verify usage line
+ assert!(response.contains("Usage"), "Missing usage information");
+ // Verify Options section
+ assert!(response.contains("Options"), "Missing Options section");
+ assert!(response.contains("--name"), "Missing --name option");
+ assert!(response.contains("-v") && response.contains("--verbose") , "Missing --verbose option");
+ assert!(response.contains("-h") && response.contains("--help"), "Missing --help option");
+ println!("β Found all options with descriptions");
+
+ println!("β All q mcp status --help content verified successfully");
+
+ Ok(())
+}
+
+#[test]
+#[cfg(all(feature = "mcp", feature = "sanity"))]
+fn test_add_and_remove_mcp_subcommand() -> Result<(), Box> {
+ println!("\nπ Testing q mcp add and remove subcommands... | Description: Tests the q mcp add and q mcp remove subcommands to add and remove MCP servers");
+
+ // First install uv dependency before starting Q Chat
+ println!("\nπ Installing uv dependency...");
+
+ std::process::Command::new("pip3")
+ .args(["install", "uv", "--break-system-packages"])
+ .output()
+ .expect("Failed to install uv");
+
+ println!("β uv dependency installed");
+
+ // First check if MCP already exists using q mcp list
+ println!("\nπ Checking if aws-documentation MCP already exists...");
+ let response = q_chat_helper::execute_q_subcommand("q", &["mcp", "list"])?;
+
+ println!("π Response: {} bytes", response.len());
+ println!("π FULL OUTPUT:");
+ println!("{}", response);
+ println!("π END OUTPUT");
+
+ // Check if aws-documentation exists in the list or config file
+ let mcp_config_exists = std::fs::read_to_string(std::env::var("HOME").unwrap_or_default() + "/.aws/amazonq/mcp.json")
+ .map(|content| content.contains("aws-documentation"))
+ .unwrap_or(false);
+
+ if response.contains("aws-documentation") && mcp_config_exists {
+ println!("\nπ aws-documentation MCP already exists, removing it first...");
+
+ let response = q_chat_helper::execute_q_subcommand("q", &["mcp", "remove", "--name", "aws-documentation"])?;
+
+ println!("π Response: {} bytes", response.len());
+ println!("π FULL OUTPUT:");
+ println!("{}", response);
+ println!("π END OUTPUT");
+
+ // Verify successful removal
+ assert!(response.contains("Removed") && response.contains("'aws-documentation'"), "Missing removal success message");
+ println!("β Successfully removed existing aws-documentation MCP");
+ } else {
+ println!("β aws-documentation MCP does not exist, proceeding with add");
+ }
+
+ // Now add the MCP server
+ println!("\nπ Executing q [subcommand]: 'q mcp add --name aws-documentation --command uvx --args awslabs.aws-documentation-mcp-server@latest'");
+ let response = q_chat_helper::execute_q_subcommand("q", &["mcp", "add", "--name", "aws-documentation", "--command", "uvx", "--args", "awslabs.aws-documentation-mcp-server@latest"])?;
+
+ println!("π Response: {} bytes", response.len());
+ println!("π FULL OUTPUT");
+ println!("{}", response);
+ println!("π END OUTPUT");
+
+ // Verify successful addition
+ assert!(response.contains("Added") && response.contains("'aws-documentation'"), "Missing success message");
+ assert!(response.contains("/.aws/amazonq/mcp.json"), "Missing config file path");
+ println!("β Found successful addition message");
+
+ // Now test removing the MCP server
+ println!("\nπ Executing q [subcommand]: 'q mcp remove --name aws-documentation'");
+ let remove_response = q_chat_helper::execute_q_subcommand("q", &["mcp", "remove", "--name", "aws-documentation"])?;
+
+ println!("π Remove response: {} bytes", remove_response.len());
+ println!("π FULL OUTPUT:");
+ println!("{}", remove_response);
+ println!("π END OUTPUT");
+
+ // Verify successful removal
+ assert!(remove_response.contains("Removed") && remove_response.contains("'aws-documentation'"), "Missing removal success message");
+ assert!(remove_response.contains("/.aws/amazonq/mcp.json"), "Missing config file path in removal");
+ println!("β Found successful removal message");
+
+ Ok(())
+}
+
+#[test]
+#[cfg(all(feature = "mcp", feature = "sanity"))]
+fn test_q_mcp_status_subcommand() -> Result<(), Box> {
+ println!("\nπ Testing q mcp status --name subcommand... | Description: Tests the q mcp status subcommand with server name to display detailed status information for a specific MCP server");
+
+ // First install uv dependency before starting Q Chat
+ println!("\nπ Installing uv dependency...");
+
+ std::process::Command::new("pip3")
+ .args(["install", "uv", "--break-system-packages"])
+ .output()
+ .expect("Failed to install uv");
+
+ println!("β uv dependency installed");
+
+ // First check if MCP already exists using q mcp list
+ println!("\nπ Checking if aws-documentation MCP already exists...");
+ let response = q_chat_helper::execute_q_subcommand("q", &["mcp", "list"])?;
+
+ println!("π Response: {} bytes", response.len());
+ println!("π FULL OUTPUT");
+ println!("{}", response);
+ println!("π END OUTPUT");
+
+ // Check if aws-documentation exists in the list or config file
+ let mcp_config_exists = std::fs::read_to_string(std::env::var("HOME").unwrap_or_default() + "/.aws/amazonq/mcp.json")
+ .map(|content| content.contains("aws-documentation"))
+ .unwrap_or(false);
+
+ if response.contains("aws-documentation") && mcp_config_exists {
+ println!("\nπ aws-documentation MCP already exists, removing it first...");
+
+ let response = q_chat_helper::execute_q_subcommand("q", &["mcp", "remove", "--name", "aws-documentation"])?;
+
+ println!("π Response: {} bytes", response.len());
+ println!("π FULL OUTPUT");
+ println!("{}", response);
+ println!("π END OUTPUT");
+
+ // Verify successful removal
+ assert!(response.contains("Removed") && response.contains("'aws-documentation'"), "Missing removal success message");
+ println!("β Successfully removed existing aws-documentation MCP");
+ } else {
+ println!("β aws-documentation MCP does not exist, proceeding with add");
+ }
+
+ // Execute mcp add command
+ println!("\nπ Executing q [subcommand]: 'q mcp add --name aws-documentation --command uvx --args awslabs.aws-documentation-mcp-server@latest'");
+ let response = q_chat_helper::execute_q_subcommand("q", &["mcp", "add", "--name", "aws-documentation", "--command", "uvx", "--args", "awslabs.aws-documentation-mcp-server@latest"])?;
+
+ println!("π Response: {} bytes", response.len());
+ println!("π FULL OUTPUT");
+ println!("{}", response);
+ println!("π END OUTPUT");
+
+ // Verify successful addition
+ assert!(response.contains("Added") && response.contains("'aws-documentation'"), "Missing success message");
+ println!("β Found successful addition message");
+
+ // Allow the tool execution
+ let response = q_chat_helper::execute_q_subcommand("q", &["mcp", "status", "--name", "aws-documentation"])?;
+
+ println!("π Allow response: {} bytes", response.len());
+ println!("π FULL OUTPUT");
+ println!("{}", response);
+ println!("π END OUTPUT");
+
+ // Verify successful status retrieval
+ assert!(response.contains("Scope"), "Missing Scope");
+ assert!(response.contains("Agent"), "Missing Agent");
+ assert!(response.contains("Command"), "Missing Command");
+ assert!(response.contains("Disabled"), "Missing Disabled");
+ assert!(response.contains("Env Vars"), "Missing Env Vars");
+
+ // Now test removing the MCP server
+ println!("\nπ Executing q [subcommand]: 'q mcp remove --name aws-documentation'");
+ let response = q_chat_helper::execute_q_subcommand("q", &["mcp", "remove", "--name", "aws-documentation"])?;
+
+ println!("π Remove response: {} bytes", response.len());
+ println!("π FULL OUTPUT");
+ println!("{}", response);
+ println!("π END OUTPUT");
+
+ // Verify successful removal
+ assert!(response.contains("Removed") && response.contains("'aws-documentation'"), "Missing removal success message");
+ assert!(response.contains("/.aws/amazonq/mcp.json"), "Missing config file path in removal");
+ println!("β Found successful removal message");
+
+ Ok(())
+}
+
diff --git a/e2etests/tests/model/mod.rs b/e2etests/tests/model/mod.rs
new file mode 100644
index 0000000000..e64adec313
--- /dev/null
+++ b/e2etests/tests/model/mod.rs
@@ -0,0 +1 @@
+pub mod test_model_dynamic_command;
\ No newline at end of file
diff --git a/e2etests/tests/test_model_dynamic_command.rs b/e2etests/tests/model/test_model_dynamic_command.rs
similarity index 60%
rename from e2etests/tests/test_model_dynamic_command.rs
rename to e2etests/tests/model/test_model_dynamic_command.rs
index 5457badb99..8127318c2c 100644
--- a/e2etests/tests/test_model_dynamic_command.rs
+++ b/e2etests/tests/model/test_model_dynamic_command.rs
@@ -1,19 +1,55 @@
-use q_cli_e2e_tests::{get_chat_session, cleanup_if_last_test};
-use std::sync::atomic::{AtomicUsize, Ordering};
+#[allow(unused_imports)]
+use q_cli_e2e_tests::q_chat_helper;
+use std::sync::{Mutex, Once, atomic::{AtomicUsize, Ordering}};
+#[allow(dead_code)]
+static INIT: Once = Once::new();
+#[allow(dead_code)]
+static mut CHAT_SESSION: Option> = None;
+#[allow(dead_code)]
+pub fn get_chat_session() -> &'static Mutex {
+ unsafe {
+ INIT.call_once(|| {
+ let chat = q_chat_helper::QChatSession::new().expect("Failed to create chat session");
+ println!("β Q Chat session started");
+ CHAT_SESSION = Some(Mutex::new(chat));
+ });
+ (&raw const CHAT_SESSION).as_ref().unwrap().as_ref().unwrap()
+ }
+}
+
+#[allow(dead_code)]
+pub fn cleanup_if_last_test(test_count: &AtomicUsize, total_tests: usize) -> Result> {
+ let count = test_count.fetch_add(1, Ordering::SeqCst) + 1;
+ if count == total_tests {
+ unsafe {
+ if let Some(session) = (&raw const CHAT_SESSION).as_ref().unwrap() {
+ if let Ok(mut chat) = session.lock() {
+ chat.quit()?;
+ println!("β Test completed successfully");
+ }
+ }
+ }
+ }
+ Ok(count)
+}
+#[allow(dead_code)]
static TEST_COUNT: AtomicUsize = AtomicUsize::new(0);
// List of covered tests
+#[allow(dead_code)]
const TEST_NAMES: &[&str] = &[
"test_model_dynamic_command",
"test_model_help_command",
+ "test_model_h_command",
];
+#[allow(dead_code)]
const TOTAL_TESTS: usize = TEST_NAMES.len();
#[test]
-#[cfg(feature = "model")]
+#[cfg(all(feature = "model", feature = "sanity"))]
fn test_model_dynamic_command() -> Result<(), Box> {
- println!("π Testing /model command with dynamic selection...");
+ println!("\nπ Testing /model command with dynamic selection... | Description: Tests the /model command interactive selection interface to choose different models and verify selection confirmation");
let session = get_chat_session();
let mut chat = session.lock().unwrap();
@@ -58,7 +94,7 @@ fn test_model_dynamic_command() -> Result<(), Box> {
// After finding prompt, parse model lines
if found_prompt {
let cleaned_line = strip_ansi(trimmed_line);
- println!("π Row: '{}' -> Cleaned: '{}'", trimmed_line, cleaned_line);
+ println!("\nπ Row: '{}' -> Cleaned: '{}'", trimmed_line, cleaned_line);
if !trimmed_line.is_empty() {
// Check if line contains a model (starts with β―, spaces, or contains model names)
@@ -69,7 +105,7 @@ fn test_model_dynamic_command() -> Result<(), Box> {
.trim()
.to_string();
- println!("π Extracted model: '{}'", model_name);
+ println!("\nπ Extracted model: '{}'", model_name);
if !model_name.is_empty() {
models.push(model_name);
}
@@ -130,9 +166,9 @@ fn test_model_dynamic_command() -> Result<(), Box> {
}
#[test]
-#[cfg(feature = "model")]
+#[cfg(all(feature = "model", feature = "sanity"))]
fn test_model_help_command() -> Result<(), Box> {
- println!("π Testing /model --help command...");
+ println!("\nπ Testing /model --help command... | Description: Tests the /model --help command to display help information for model selection functionality");
let session = get_chat_session();
let mut chat = session.lock().unwrap();
@@ -144,10 +180,6 @@ fn test_model_help_command() -> Result<(), Box> {
println!("{}", response);
println!("π END OUTPUT");
- /* Verify description
- assert!(response.contains("Select") && response.contains("model"), "Missing model selection description");
- println!("β Found model selection description");*/
-
// Verify Usage section
assert!(response.contains("Usage:"), "Missing Usage section");
assert!(response.contains("/model"), "Missing /model command in usage section");
@@ -158,8 +190,7 @@ fn test_model_help_command() -> Result<(), Box> {
println!("β Found Options section");
// Verify help flags
- assert!(response.contains("-h") && response.contains("--help") && response.contains("Print help"), "Missing -h, --help flags");
- assert!(response.contains("Print help"), "Missing Print help description");
+ assert!(response.contains("-h") && response.contains("--help"), "Missing -h, --help flags");
println!("β Found help flags: -h, --help with Print help description");
println!("β All model help content verified!");
@@ -172,3 +203,42 @@ fn test_model_help_command() -> Result<(), Box> {
Ok(())
}
+
+#[test]
+#[cfg(all(feature = "model", feature = "sanity"))]
+fn test_model_h_command() -> Result<(), Box> {
+ println!("\nπ Testing /model -h command... | Description: Tests the /model -h command (short form) to display help information for model selection functionality");
+
+ let session = get_chat_session();
+ let mut chat = session.lock().unwrap();
+
+ let response = chat.execute_command("/model -h")?;
+
+ println!("π Model help response: {} bytes", response.len());
+ println!("π FULL OUTPUT:");
+ println!("{}", response);
+ println!("π END OUTPUT");
+
+ // Verify Usage section
+ assert!(response.contains("Usage:"), "Missing Usage section");
+ assert!(response.contains("/model"), "Missing /model command in usage section");
+ println!("β Found Usage section with /model command");
+
+ // Verify Options section
+ assert!(response.contains("Options:"), "Missing Options section");
+ println!("β Found Options section");
+
+ // Verify help flags
+ assert!(response.contains("-h") && response.contains("--help"), "Missing -h, --help flags");
+ println!("β Found help flags: -h, --help with Print help description");
+
+ println!("β All model help content verified!");
+
+ // Release the lock before cleanup
+ drop(chat);
+
+ // Cleanup only if this is the last test
+ cleanup_if_last_test(&TEST_COUNT, TOTAL_TESTS)?;
+
+ Ok(())
+}
\ No newline at end of file
diff --git a/e2etests/tests/q_subcommand/mod.rs b/e2etests/tests/q_subcommand/mod.rs
new file mode 100644
index 0000000000..a7257d9716
--- /dev/null
+++ b/e2etests/tests/q_subcommand/mod.rs
@@ -0,0 +1,7 @@
+pub mod test_q_chat_subcommand;
+pub mod test_q_doctor_subcommand;
+pub mod test_q_translate_subcommand;
+pub mod test_q_setting_subcommand;
+pub mod test_q_whoami_subcommand;
+pub mod test_q_debug_subcommand;
+pub mod test_q_inline_subcommand;
\ No newline at end of file
diff --git a/e2etests/tests/q_subcommand/test_q_chat_subcommand.rs b/e2etests/tests/q_subcommand/test_q_chat_subcommand.rs
new file mode 100644
index 0000000000..f35b9d7a39
--- /dev/null
+++ b/e2etests/tests/q_subcommand/test_q_chat_subcommand.rs
@@ -0,0 +1,29 @@
+#[allow(unused_imports)]
+use q_cli_e2e_tests::q_chat_helper;
+
+#[test]
+#[cfg(all(feature = "q_subcommand", feature = "sanity"))]
+fn test_q_chat_subcommand() -> Result<(), Box> {
+ println!("\nπ Testing q chat subcommand... | Description: Tests the q chat subcommand that opens Q terminal for interactive AI conversations.");
+
+ println!("\nπ Executing 'q chat' subcommand...");
+ let response = q_chat_helper::execute_q_subcommand("q", &["chat", "\"what is aws?\""])?;
+
+ println!("π Chat response: {} bytes", response.len());
+ println!("π FULL OUTPUT:");
+ println!("{}", response);
+ println!("π END OUTPUT");
+
+ // Validate we got a proper AWS response
+ assert!(response.contains("Amazon Web Services") || response.contains("AWS"),
+ "Response should contain AWS information");
+ assert!(response.len() > 100, "Response should be substantial");
+
+ println!("β Got substantial AI response ({} bytes)!", response.len());
+
+ println!("β Chat subcommand executed!");
+
+ println!("β q chat subcommand executed successfully!");
+
+ Ok(())
+}
\ No newline at end of file
diff --git a/e2etests/tests/q_subcommand/test_q_debug_subcommand.rs b/e2etests/tests/q_subcommand/test_q_debug_subcommand.rs
new file mode 100644
index 0000000000..df10882df2
--- /dev/null
+++ b/e2etests/tests/q_subcommand/test_q_debug_subcommand.rs
@@ -0,0 +1,205 @@
+#[allow(unused_imports)]
+use q_cli_e2e_tests::q_chat_helper;
+
+#[test]
+#[cfg(all(feature = "q_subcommand", feature = "sanity"))]
+fn test_q_debug_subcommand() -> Result<(), Box> {
+ println!("\nπ Testing q debug subcommand... | Description: Tests the q debug subcommand that provides debugging utilities for the app including app debugging, build switching, logs viewing, and various diagnostic tools.");
+
+ println!("\nπ Executing 'q debug' subcommand...");
+ let response = q_chat_helper::execute_q_subcommand("q", &["debug"])?;
+
+ println!("π Debug response: {} bytes", response.len());
+ println!("π FULL OUTPUT:");
+ println!("{}", response);
+ println!("π END OUTPUT");
+
+ // Assert debug help output contains expected commands
+ assert!(response.contains("Debug the app"), "Response should contain debug description");
+ assert!(response.contains("Commands:"), "Response should list available commands");
+ assert!(response.contains("app"), "Response should contain 'app' command");
+ assert!(response.contains("build"), "Response should contain 'build' command");
+ assert!(response.contains("logs"), "Response should contain 'logs' command");
+
+ println!("β Got debug help output ({} bytes)!", response.len());
+ println!("β q debug subcommand executed successfully!");
+
+ Ok(())
+}
+
+#[test]
+#[cfg(all(feature = "q_subcommand", feature = "sanity"))]
+fn test_q_debug_app_subcommand() -> Result<(), Box> {
+ println!("\nπ Testing q debug app subcommand... | Description: Tests the q debug app subcommand that provides debugging utilities for the app including app debugging, build switching, logs viewing, and various diagnostic tools.");
+
+ println!("\nπ Executing 'q debug app' subcommand...");
+ let response = q_chat_helper::execute_q_subcommand("q", &["debug", "app"])?;
+
+ println!("π Debug response: {} bytes", response.len());
+ println!("π FULL OUTPUT:");
+ println!("{}", response);
+ println!("π END OUTPUT");
+
+ // Assert that q debug app launches the Amazon Q interface
+ assert!(response.contains("Amazon Q"), "Response should contain 'Amazon Q'");
+ assert!(response.contains("π€ You are chatting with"), "Response should show chat interface");
+
+ println!("β Got debug app output ({} bytes)!", response.len());
+ println!("β q debug app subcommand executed successfully!");
+
+ Ok(())
+}
+
+#[test]
+#[cfg(all(feature = "q_subcommand", feature = "sanity"))]
+fn test_q_debug_help_subcommand() -> Result<(), Box> {
+ println!("\nπ Testing q debug --help subcommand... | Description: Tests the q debug --help subcommand to validate help output format and content.");
+
+ println!("\nπ Executing 'q debug --help' subcommand...");
+ let response = q_chat_helper::execute_q_subcommand("q", &["debug", "help"])?;
+
+ println!("π Debug response: {} bytes", response.len());
+ println!("π FULL OUTPUT:");
+ println!("{}", response);
+ println!("π END OUTPUT");
+
+ // Assert debug help output contains expected commands
+ assert!(response.contains("Usage:") && response.contains("q debug") && response.contains("[OPTIONS]") && response.contains(""),
+ "Help should contain usage line");
+ assert!(response.contains("Commands:"), "Response should list available commands");
+ assert!(response.contains("app"), "Response should contain 'app' command");
+ assert!(response.contains("build"), "Response should contain 'build' command");
+ assert!(response.contains("logs"), "Response should contain 'logs' command");
+ assert!(response.contains("Options:"),
+ "Help should contain Options section");
+ assert!(response.contains("-v, --verbose"),
+ "Help should contain verbose option");
+ assert!(response.contains("-h, --help"),
+ "Should contain help option");
+
+ println!("β Got debug help output ({} bytes)!", response.len());
+ println!("β q debug --help subcommand executed successfully!");
+
+ Ok(())
+}
+
+#[test]
+#[cfg(all(feature = "q_subcommand", feature = "sanity"))]
+fn test_q_debug_build_help() -> Result<(), Box> {
+ println!("\nπ Testing q debug build --help subcommand... | Description: Tests the q debug build --help subcommand to validate help output format and available build options.");
+
+ println!("\nπ Executing 'q debug build --help' subcommand...");
+ let response = q_chat_helper::execute_q_subcommand("q", &["debug", "build", "--help"])?;
+
+ println!("π Debug response: {} bytes", response.len());
+ println!("π FULL OUTPUT:");
+ println!("{}", response);
+ println!("π END OUTPUT");
+
+ // Assert expected output
+ assert!(response.contains("Usage: q debug build [OPTIONS] [BUILD]"), "Response should contain usage line");
+ assert!(response.contains(""), "Response should contain APP argument");
+ assert!(response.contains("[BUILD]"), "Response should contain BUILD argument");
+ assert!(response.contains("-v, --verbose... Increase logging verbosity"), "Response should contain verbose option");
+ assert!(response.contains("-h, --help Print help"), "Response should contain help option");
+
+ println!("β Got debug build help output ({} bytes)!", response.len());
+ println!("β q debug build --help subcommand executed successfully!");
+
+ Ok(())
+}
+
+#[test]
+#[cfg(all(feature = "q_subcommand", feature = "sanity"))]
+fn test_q_debug_build_autocomplete() -> Result<(), Box> {
+ println!("\nπ Testing q debug build autocomplete subcommand... | Description: Tests the q debug build autocomplete subcommand to get current autocomplete build version.");
+
+ println!("\nπ Executing 'q debug build autocomplete' subcommand...");
+ let response = q_chat_helper::execute_q_subcommand("q", &["debug", "build", "autocomplete"])?;
+
+ println!("π Debug response: {} bytes", response.len());
+ println!("π FULL OUTPUT:");
+ println!("{}", response);
+ println!("π END OUTPUT");
+
+ // Assert expected output (should be either "production" or "beta")
+ assert!(response.contains("production") || response.contains("beta"), "Response should contain either 'production' or 'beta'");
+
+ println!("β Got debug build autocomplete output ({} bytes)!", response.len());
+ println!("β q debug build autocomplete subcommand executed successfully!");
+
+ Ok(())
+}
+
+#[test]
+#[cfg(all(feature = "q_subcommand", feature = "sanity"))]
+fn test_q_debug_build_dashboard() -> Result<(), Box> {
+ println!("\nπ Testing q debug build dashboard subcommand... | Description: Tests the