From 6310ae00c24abce28356bf5900a546d20de5a5b6 Mon Sep 17 00:00:00 2001 From: abhraina-aws Date: Wed, 5 Nov 2025 05:48:30 -0800 Subject: [PATCH 1/2] Initial Commit --- DELETEMEFILE | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 DELETEMEFILE diff --git a/DELETEMEFILE b/DELETEMEFILE new file mode 100644 index 0000000000..e69de29bb2 From 158f31c86461f98bd125038b7872e6bc2fa4641f Mon Sep 17 00:00:00 2001 From: Abhishek Anne Date: Thu, 6 Nov 2025 03:31:02 +0530 Subject: [PATCH 2/2] feat: Add comprehensive E2E tests for QCli (#3370) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Initial Framework with working tests * QCLI Automation for Basic chat * QCLI automation for Compact,MCP,Usage,Hooks,Subscribe,Model tests * Q CLI Automation for context and help * Q-CLI automated testcases for Agent commands * feat: added a better way to run tests categorized by features * fix: added an updated readme * Q CLI Automation for basic chat and context * QCLI Automation for Tools,Issue,Promt tests * Q CLI Automation for MCP * QCLI automation prompt list command * QCLI Automation for MCP * QCLI test case automation for dynamic model selection and bug fix for MCP commands * QCLI Automation for MCP tests * QCLI Automation for tools,model,editor,subscribe,usage,compact,hooks,issues,tests * QCLI Test automation - merged agent feature test into one * QCLI Test automation - merged context feature test into one * QCLI Test automation - seperated common methods * QCLI Test automation - merged ai_prompts feature test into one * QCLI Test automation - merged agent feature test into one * QCLI Automation for MCP Feature * QCLI Automation for hooks,usage,issue,tools,subscribe,model,compact,editor tests * Added some additional scripts for specific runs * Refactor: Merge save/load into one file, remove verbose from MCP context * Remove failing MCP test case * Created folder structure for e2e test * Fixed the warnings in code * Modified the tests features to support multiple features * QCLI Automation for core_session,integration,mcp,model,session_mgmt tests * Reduce read_response timeout from 20s to 4s * Refactor: Remove verbose from MCP context and save/load and fixed the failing test case for adding multiple file as context. * Migrated to python script with HTML report generation * Add new tests cases test_add_and_remove_mcp_command and test_q_mcp_status_command * added html template * Test wise console output * Test wise console output for single thread * Move reports to reports folder * Script updated to support test description * Added description to agent and usage command tests , add .gitignore * Fixed individual test output * Removed all regression test cases * Added descriptions to tests * Modfied system info * Fixed the Suite tital to capitalize * Clean console output * Removed the tiles from summary section , added table in HTML report * Added histogram for the test pass fail * Validate the user input for passed features * Fixed the summary cards layout * Sending prompts used execute command instead of send prompts * Fixed graph hover text label * Added highlights on failed features * QCLI Automation for tools * move common functions from lib to individual test files to fix intermittent /quit issue, fix MCP failures, and add compact feature tests * add compact new tests and fix chat function for help,clear,ai_prompts tests * Add new test cases related to tools and fix the warning * Updated help section of script * Added executng animation while executing cargo command * Improved quiet mode * Add detail descriptions to 5 test cases related to tools. * add new tests for editor feature * Fixed quiet mode * Fixed quiet mode with logs * add new tests for editor,compact feature and description for editor , comoact feature test cases * Fixed order of features * Add Q subcommand support and corresponding tests - Introduced `q_subcommand` feature in Cargo.toml - Implemented functions for starting and executing Q subcommands in lib.rs - Added tests for `q chat`, `q doctor`, and `q translate` commands - Updated all_tests.rs to include the new q_subcommand tests * added new test case for compact feature * Improved animation * Add regression tests for MCP feature * Modified the report css for code * Refactor Q CLI subcommand execution and enhance tests - Replaced `start_q_subcommand` and `read_session_response` with `execute_q_subcommand` and `execute_q_subcommand_with_stdin` for improved command execution and response handling. - Added new tests for `q mcp` subcommands, verifying help commands and functionality. - Enhanced existing test descriptions for clarity and consistency. * Add tests for q settings and q whoami subcommands * Add tests for q debug and q inline subcommands * Refactor test descriptions to use HTML code tags for command formatting - Updated test output messages in various test files to wrap command names in tags for better readability. * Enhance MCP subcommand tests and add q debug build related tests * Add help subcommand tests for q inline commands and Add todos command support and enhance related tests * Updated readme file , wrokspace clean up * Add tests for /todos resume and /todos delete commands, enhance existing tests * Added tests for q whoami subcommand and fixed failing tests for features agebt,integarion and q-subcommand * add 3 tests for experiment commands feature * Increase time limit to improve command response handling and prevent premature termination * Add changelog command and experiment feature tests * Added 3 tests under q_subcommand feature * Refactor test files to utilize centralized chat session management - Removed redundant chat session code from individual test files. * added 2 tests for q_subcommand * QCLI e2etesting - fixed warnings * feat(knowledge): implement pattern filtering and settings integration (TI-3, TI-5) (#2545) - Add pattern filtering foundation with --include/--exclude CLI flags (TI-3) - Implement knowledge settings integration with database system (TI-5) - Add comprehensive pattern filter module with glob-style support - Enhance file processor with async pattern filtering capabilities - Add extensive test coverage for pattern filtering functionality - Update knowledge CLI with improved error handling and validation - Add settings support for chunk size, overlap, and file limits Co-authored-by: Kenneth S. * fix: Add client-side modelName mapping for backward compatibility (#2604) * add mapping * adjust import location * compare name first * feat: add delay tracking interceptor for retry notifications (#2607) * fix(agent): tool permission override (#2606) * fix: add more safety checks for execute_bash readonly checks (#2605) * fix: change fallback (#2615) * chore: bump version to 1.14.0 (#2616) * Revert "fix(agent): tool permission override (#2606)" (#2618) This reverts commit dfaa58083f49659bd3f512642c0b4fead1d0482c. * Knowledge beta improvements phase 2: Refactor async_client and add support for BM25 (#2608) * [feat] Adds support to embedding-type - Remove unused BM25TextEmbedder from embedder factory - Replace with MockTextEmbedder for Fast embedding type - Remove bm25.rs file and related imports/exports - Fix BM25Context and SemanticContext to save data after adding points - Fix BM25 data filename from bm25_data.json to data.bm25.json - Add base_dir storage to ContextManager for proper path resolution - Major refactoring to async context management with background operations - Adds separate optimized index for bm25 - Fix all clippy warnings and remove dead code BM25 search now works correctly with persistent contexts. * fix: Update cancel_most_recent_operation to use OperationManager - Fix cancel_most_recent_operation to delegate to OperationManager instead of accessing active_operations directly - Add missing cancel_most_recent_operation method to OperationManager - Ensures proper separation of concerns in the refactored architecture * fix: Remove BM25 from benchmark tests - Remove BM25TextEmbedder references from benchmark_test.rs - Remove benchmark_bm25_model function - Keep only Candle model benchmarks - Fixes compilation error after BM25TextEmbedder removal * docs: Update semantic-search-client README for index types - Update description to mention BM25 and vector embeddings - Add Multiple Index Types feature - Update Embeddings section to Index Types section - Remove ONNX references (no longer supported) - Reflect Fast (BM25) vs Best (Semantic) terminology - Update section headers for consistency * fix: remove auto-save from context add_data_points methods - Remove automatic save() calls from add_data_points in both semantic_context.rs and bm25_context.rs - Add explicit save() calls in context_creator.rs after data addition is complete - Improves performance by avoiding multiple disk writes during batch operations - Addresses PR #2608 feedback about inefficient disk I/O on each context addition * fix: resolve compilation error and operation cancel warnings - Fix return type mismatch in knowledge_store.rs cancel_operation method - Change cancel_most_recent_operation to return Ok instead of Err when no operations exist - Eliminates 'Failed to cancel operations' warnings when no operations are active * fix: improve error handling and code cleanup - Update error handling in knowledge_store.rs - Clean up context_creator.rs formatting and comments --------- Co-authored-by: Kenneth S. * fix: use_aws printing default profile when its not used, minor updates to agent docs (#2617) * chore(models): change fallback model, align with all clients (#2624) * feat: add github action for release notification (#2625) * feat: add github action for release notification * feat(agent): hot swap (#2637) * changes prompt list result to be sent over via messenger * changes tool manager orchestrator tasks to keep prompts * changes mpsc to broadcast * restores prompt list functionality * restore prompt get functionality * adds api on tool manager to hotswap * spawns task to send deinit msg via messenger * adds slash command to hotswap agent * modifies load tool wait time depending on context * adds comments to retry logic for prompt completer * fixes lint * adds pid field to messenger message * adds interactive menu for swapping agent * fixes stale mcp load record * documents build method on tool manager builder and refactor to make the build method smaller * feat: Implement wildcard pattern matching for agent allowedTools (#2612) - Add globset-based pattern matching to support wildcards (* and ?) in allowedTools - Create util/pattern_matching.rs module with matches_any_pattern function - Update all native tools (fs_read, fs_write, execute_bash, use_aws, knowledge) to use pattern matching - Update MCP custom tools to support wildcard patterns while preserving exact server-level matching - Standardize imports across tool files for consistency - Maintain backward compatibility with existing exact-match behavior Enables agent configs like: - "fs_*" matches fs_read, fs_write - "@mcp-server/tool_*" matches tool_read, tool_write - "execute_*" matches execute_bash, execute_cmd * fixes unwrap on pid (#2657) * ci fix (#2658) * feat: added mcp admin level configuration with GetProfile (#2639) * first pass * add notification when /mcp & /tools * clear all tool related filed in agent * store mcp_enabled in chatsession & conversationstate * delete duplicate api call * set mcp_enabled value after load * remove clear mcp configs method * clippy * remain@builtin/ and *, add a ut for clear mcp config * automated agent edit test case. * reduced timeout duration and added test-agent-edit * added one test case for settings format * added changes for mod.rs * code fix * fix(agent): tool permission (#2619) * adds warnings for when tool settings are overridden by allowed tools * adjusts tool settings eval order * modifies doc * moves warning to be displayed after splash screen * canonicalizes paths prior to making glob sets * simplifies overridden warning message printing logic * adds more doc on path globbing * chore: bumps version to 1.14.1 (#2662) * adding telemetry for mcp tool names available and mcp tool names selected (#2655) * feat: add tangent mode for context isolated conversations (#2634) - Add /tangent command to toggle between normal and tangent modes - Preserve conversation history during tangent for context - Restore main conversation when exiting tangent mode - Add Ctrl+T keyboard shortcut for quick access - Visual indicator: green [T] prompt in tangent mode - Comprehensive tests for state management and UI components Allows users to explore side topics without polluting main conversation context. * Apply cargo +nightly fmt formatting (#2664) Co-authored-by: Kenneth S. * feat: implement agent-scoped knowledge base and context-specific search (#2647) * Short-Term fix for SendTelemetry API Validation errors (#2694) * feat: add introspect tool for Q CLI self-awareness (#2677) - Add introspect tool with comprehensive Q CLI documentation - Include auto-tangent mode for isolated introspect conversations - Add GitHub links for documentation references - Support agent file locations and built-in tools documentation - Add automatic settings documentation with native enum descriptions - Use strum EnumMessage for maintainable setting descriptions * Format fix (#2697) Co-authored-by: Kenneth S. * Update q-developer smithy clients and reformat files (#2698) * Update q-developer smithy clients and reformat files * Fix formatting * feat: add tangent mode duration tracking telemetry (#2710) - Track time spent in tangent mode sessions - Send duration metric when exiting tangent mode via /tangent command - Use codewhispererterminal_chatSlashCommandExecuted metric with: - command: 'tangent' - subcommand: 'exit' - value: duration in seconds - Add comprehensive tests for duration calculation - Enables analytics on tangent mode usage patterns * feat: add to-do list functionality to QCLI (#2533) * first round changes for agent generate for workshopping (#2690) * adding in agent contribution metric * chore: fix lints * agent generate where user is prompted to include or exclude entire MCP servers * rebasing changes to newest api for conversation.rs * making changes according to Brandon's feedback * clippy * suppress warning messages * making changes after code review * felix feedback changes --------- Co-authored-by: Xian Wu Co-authored-by: Brandon Kiser * fix: update per-prompt timestamp to include local time zone information (#2654) * feat: add /experiment slash command for toggling experimental features (#2711) * feat: add /experiment slash command for toggling experimental features - Add new /experiment command - Toggle selection interface with colored On/Off status indicators - Include experiment descriptions and disclaimer about experimental features - Include documentation so its available in instrospect tool * Visual apperance fix * Format fix --------- Co-authored-by: Kenneth S. * chore: bump version to 1.15.0 (#2719) * feat: added similar feature gating of tangent to tangent mode and introspect tool (#2720) * chore: add /tangent to /experiment (#2721) * fix: added summary to tangent and showed correct display label for introspect (#2725) * docs: added the documentation for introspect in the built in tools. (#2727) * this contains bug fix from the bug bash for agent generate (#2732) * fixing bugs * formatting --------- Co-authored-by: Xian Wu * fix: Fixes out of bounds issue with dropdown. (#2726) * Update retry-interceptor warning message (#2709) * Add telemetry support for agent contribution tracking (#2699) - Refactor send_cw_telemetry to handle multiple event types - Add AgentContribution event handling that sends ChatInteractWithMessageEvent - Track accepted line count from agent contributions as AgenticCodeAccepted interaction type * fix: fix to-do list bugs (#2729) * fix: use abs value of lines added/removed by q-cli (#2737) * fix: make todo lists an experimental feature (#2740) * fix: CTRL+C handling during multi-select, auto completion for /agent generate (#2741) * fixing bugs * formatting * fix: CTRL+C handling during multi-select, auto completion for /agent generate * set use legacy mcp config to false --------- Co-authored-by: Xian Wu * Fix calculation for num-lines contributed by q-cli (#2738) * Update knowledge base directory path documentation (#2763) - Changed from ~/.q/knowledge_bases/ to ~/.aws/amazonq/knowledge_bases/ - Default agent uses q_cli_default/ (no alphanumeric suffix) - Custom agents use _/ format Co-authored-by: Kenneth S. * docs: Update todo list docs for introspect (#2776) * feat: added tangent & introspect docs & provided to introspect (#2775) * feat: implement persistent CLI history with file storage (#2769) - Add Drop trait to InputSource for automatic history saving - Replace DefaultHistory with FileHistory for persistence - Store history in ~/.aws/amazonq/cli_history - Refactor ChatHinter to use rustyline's built-in history search - Remove manual history tracking in favor of rustyline's implementation - Add history loading on startup with error handling - Clean up unused hinter history update methods * chore: Skip sending profileArn when using custom endpoints (#2777) * comment profile set * comment profile in apiclient * add a helper func * fix compile issue * remove dead code tag * chore(mcp): migrate to rmcp (#2700) * client struct definition * clean up unused code * adds mechanism for checking if server is alive * prefetches prompts if applicable * fixes agent swap fixes agent swap * only applies process group leader promo for unix * removes unused import for windows * renames abstractions for different stages of mcp config * Dont preserve summary when conversation is cleared (#2793) * feat: add AGENTS.md to default agent resources (#2812) - Add file://AGENTS.md to default resources list alongside AmazonQ.md - Update test to include both AmazonQ.md and AGENTS.md files - Ensures AGENTS.md is included everywhere AmazonQ.md was previously included Co-authored-by: Matt Lee * feat: add model field support to agent format (#2815) - Add optional 'model' field to Agent struct for specifying model per agent - Update JSON schema and documentation with model field usage - Integrate agent model into model selection priority: 1. CLI argument (--model) 2. Agent's model field (new) 3. User's saved default model 4. System default model - Add proper fallback when agent specifies unavailable model - Extract fallback logic to eliminate code duplication - Include comprehensive unit tests for model field functionality - Maintain backward compatibility with existing agent configurations Co-authored-by: Matt Lee * chore: updating doc to surface /agent generate and note block for /knowledge (#2823) * Properly handle path with trailing slash in file matching (#2817) * Properly handle path with trailing slash in file matching Today if a path has a trailing slash, the glob pattern will look like "/path-to-folder//**" (note the double slash). Glob doesn't work with double slash actually (it doesn't match anything). As a result, the permission management for fs_read and fs_write is broken when allowed or denied path has trailing slash. The fix is to just manually remove the trailing slash. * format change * Fix: Add configurable line wrapping for chat (#2816) * add a wrapmode in chat args * add a ut for the wrap arg * feat(use_aws): add configurable autoAllowReadonly setting (#2828) - Add auto_allow_readonly field to use_aws Settings struct (defaults to false) - Update eval_perm method to use auto_allow_readonly setting instead of hardcoded behavior - Default behavior: all AWS operations require user confirmation (secure by default) - Opt-in behavior: when autoAllowReadonly=true, read-only operations are auto-approved - Add comprehensive tests covering all scenarios - Maintains backward compatibility through configuration ๐Ÿค– Assisted by Amazon Q Developer Co-authored-by: Matt Lee * feat: add auto-announcement feature with /changelog command (#2833) * feat(mcp): enables remote mcp (#2836) * feat: Add /tangent tail to preserve the last tangent conversation (#2838) Users can now keep the final question and answer from tangent mode by using `/tangent tail` instead of `/tangent`. This preserves the last Q&A pair when returning to the main conversation, making it easy to retain helpful insights discovered during exploration. - `/tangent` - exits tangent mode (existing behavior unchanged) - `/tangent tail` - exits tangent mode but keeps the last Q&A pair This enables users to safely explore topics without losing the final valuable insight that could benefit their main conversation flow. * feat: add daily heartbeat telemetry (#2839) Tracks daily active users by sending amazonqcli_dailyHeartbeat event once per day. Uses fail-closed logic to prevent spam during database errors. * fix: update dangerous patterns for execute bash to include $ (#2811) * docs: fix local agent directory path in documentation (#2749) * docs: fix local agent directory path - Fix local agent path from .aws/amazonq/cli-agents/ to .amazonq/cli-agents/ - Global paths (~/.aws/amazonq/cli-agents/) remain correct - Aligns documentation with source code implementation * fix: correct workspace agent path in /agent help message The help message for the /agent command incorrectly showed the workspace agent path as 'cwd/.aws/amazonq/cli-agents' when it should be 'cwd/.amazonq/cli-agents' (without the .aws directory). This fix aligns the help text with the actual WORKSPACE_AGENT_DIR_RELATIVE constant defined in directories.rs. * Invalid pointer to trace log location (#2734) * Fix bug README.md (#2569) * fix: Layout fix (#2798) * docs: Update experiment docs to contain todo lists (#2791) * feat: Add support for comma-containing arguments in MCP --args parameter (#2754) * chore: add extra curl flags for debugging build during feed.json failures (#2843) * fix: remove downloading feed during build (#2844) * feat(execute_bash): change autoAllowReadonly default to false for security (#2846) - Change default_allow_read_only() from true to false for secure by default behavior - Default behavior: all bash commands require user confirmation (secure by default) - Opt-in behavior: when autoAllowReadonly=true, read-only commands are auto-approved - Use autoAllowReadonly casing to match use_aws tool pattern - Update documentation to reflect new default value and consistent naming - Add comprehensive tests covering all scenarios - Maintains backward compatibility through configuration - Follows same pattern as use_aws autoAllowReadonly setting ๐Ÿค– Assisted by Amazon Q Developer Co-authored-by: Matt Lee * feat(agent): add edit subcommand to modify existing agents (#2845) - Add Edit subcommand to AgentSubcommands enum - Implement edit functionality that opens existing agent files in editor - Use Agent::get_agent_by_name to locate and load existing agents - Include post-edit validation to ensure JSON remains valid - Add comprehensive tests for the new edit subcommand - Support both --name and -n flags for agent name specification ๐Ÿค– Assisted by Amazon Q Developer Co-authored-by: Matt Lee * fix(mcp): not being able to refresh tokens for remote mcp (#2849) * adds registration persistance for token refresh * truncates on tool description * Modifies oauth success message * adds time stamps on mcp logs * fix(agent): add edit subcommand support to /agent slash command (#2854) - Add Edit variant to AgentSubcommand enum in profile.rs - Implement edit functionality for slash command usage - Use Agent::get_agent_by_name to locate existing agents - Include post-edit validation and agent reloading - Add edit case to name() method for proper command routing - Enables /agent edit --name usage in chat sessions ๐Ÿค– Assisted by Amazon Q Developer Co-authored-by: Matt Lee * fixes creation of cache directory panic when path already exists (#2857) * Reduce default fs_read trust permission to current working directory only (#2824) * Reduce default fs_read trust permission to current working directory only Previously by default fs_read is trusted to read any file on user's file system. This PR reduces the fs_read permission to CWD only. This means user can still access any file under CWD without prompt. But if user needs to access file outside CWD, she will be prompted for explicit approval. User can still explicitly add fs_read to trusted tools in chat / agent definition so fs_read can read any file without prompt. This change essentially adds a layer of defense against prompt injection by following the least-privilege principle. * remove allow_read_only since it is always false now * fixes remote mcp creds not being written when obtained (#2878) * fixes bug where refreshed credentials gets deleted (#2879) * fix(mcp): bug where refreshed credentials gets deleted (#2880) * Update bm25 to v2.3.2 and ignore unmaintained fxhash advisory (#2872) - Updated bm25 from v2.2.1 to v2.3.2 (latest version) - Added RUSTSEC-2025-0057 to deny.toml ignore list for unmaintained fxhash - fxhash is a transitive dependency of bm25, waiting for upstream migration to rustc-hash Co-authored-by: Kenneth S. * adds temp message to still loading section of /tools (#2881) * chore: removes codeowner for schema (#2892) * chore(agent): updates agent config schema (#2891) * chore: version bump (#2894) * fix format (#2895) * fix(mcp): shell expansion not being done for stdio command (#2915) * Bump up version for hotfix (#2916) * chore: add 1.16.1 feed.json entry (#2919) * Change autocomplete shortcut from ctrl-f to ctrl-g (#2825) * Change autocomplete shortcut from ctrl-f to ctrl-g The reason is ctrl-f is the standard shortcut in UNIX for moving cursor forward by 1 character. You can find it being supported everywhere... in your browser, your terminal, etc. * make the autocompletion key configurable * feat: add support for preToolUse and postToolUse hook (#2875) * fix(mcp): oauth issues (#2925) * fix incorrect scope for mcp oauth * reverts custom tool config enum change * fixes display task overriding sign in notice * updates schema * fix(chat): reset pending tool state when clearing conversation (#2855) Reset tool_uses, pending_tool_index, and tool_turn_start_time to prevent orphaned tool approval prompts after conversation history is cleared. Co-authored-by: Niraj Chowdhary * Trim region to avoid login failures (#2930) * chore: copy change for warning message for oauth redirect page (#2931) * fix: removes deny unknown fields in mcp config (#2935) * Normalize and expand relative-paths to absolute paths (#2933) * Bump version to 1.16.2 and update feed.json (#2938) * changes how mcp command gets expanded (#2940) * Update default tool label for execute bash (#2945) * fix: incorrect wrapping for response text (#2900) * Improve error messages for dispatch failures (#2969) * automated tangent and agent generate command. * automated introspect command and handle wanrnings. * added code to automate /prompts get command * fixed the html rendering issue. * automated whoami command and handle html color rendering issue. * Modified histogram bar labels * Added support for custom timeout * added multiline test case automation and handle warning * automated ctrl+s command and refactor ctrl+j command for multiline. * added test case automation support for /agent swap * added test case automation for alt+enter and added timeout to existing function for long execution. * Added python script to analyse the qcli e2e test performances * Changes for improving test case execution. * Q CLi e2e testing updated python script to analyse the qcli e2e test performances * fixed failed test cases because of latest q cli version and remove all warnings. * Q Cli e2e testing automation report performance review dashboard modified. * code changes for /agent schema test case automation * code changes to automate q settings delete command * added code to automate q quit subcommand. * added code to automate /todos clear-finished command. * Q cli e2e tests fix warnings --------- Co-authored-by: abhraina-aws Co-authored-by: Shreya [C] Bhagat Co-authored-by: sayema Anjum Co-authored-by: Kenneth Sanchez V Co-authored-by: Kenneth S. Co-authored-by: evanliu048 Co-authored-by: kkashilk <93673379+kkashilk@users.noreply.github.com> Co-authored-by: Felix Ding Co-authored-by: Brandon Kiser <51934408+brandonskiser@users.noreply.github.com> Co-authored-by: Nitish [C] Dhok Co-authored-by: xianwwu Co-authored-by: kiran-garre <137448023+kiran-garre@users.noreply.github.com> Co-authored-by: Xian Wu Co-authored-by: Brandon Kiser Co-authored-by: Justin Moser Co-authored-by: Matt Lee <1302416+mr-lee@users.noreply.github.com> Co-authored-by: Matt Lee Co-authored-by: Erben Mo Co-authored-by: yayami3 <116920988+yayami3@users.noreply.github.com> Co-authored-by: Bart van Bragt Co-authored-by: Ennio Pastore Co-authored-by: Michael Orlov <34108460+harleylrn@users.noreply.github.com> Co-authored-by: Matt Lee Co-authored-by: Erben Mo Co-authored-by: nirajchowdhary <226941436+nirajchowdhary@users.noreply.github.com> Co-authored-by: Niraj Chowdhary --- e2etests/.gitignore | 4 + e2etests/Cargo.lock | 202 +++++ e2etests/Cargo.toml | 49 + e2etests/README.md | 352 +++++++ e2etests/analysis_report_template.html | 857 ++++++++++++++++++ e2etests/analyze_reports.py | 455 ++++++++++ e2etests/html_template.html | 258 ++++++ e2etests/run_tests.py | 649 +++++++++++++ e2etests/src/lib.rs | 262 ++++++ e2etests/tests/agent/mod.rs | 2 + e2etests/tests/agent/test_agent_commands.rs | 556 ++++++++++++ e2etests/tests/ai_prompts/mod.rs | 2 + e2etests/tests/ai_prompts/test_ai_prompt.rs | 98 ++ .../tests/ai_prompts/test_prompts_commands.rs | 160 ++++ e2etests/tests/all_tests.rs | 21 + e2etests/tests/context/mod.rs | 1 + .../tests/context/test_context_command.rs | 497 ++++++++++ e2etests/tests/core_session/mod.rs | 6 + .../core_session/test_changelog_command.rs | 79 ++ .../tests/core_session/test_clear_command.rs | 44 + .../core_session/test_command_introspect.rs | 33 + .../core_session/test_command_tangent.rs | 28 + .../tests/core_session/test_help_command.rs | 173 ++++ .../tests/core_session/test_quit_command.rs | 20 + e2etests/tests/experiment/mod.rs | 1 + .../experiment/test_experiment_command.rs | 496 ++++++++++ e2etests/tests/integration/mod.rs | 4 + .../integration/test_editor_help_command.rs | 290 ++++++ .../tests/integration/test_hooks_command.rs | 98 ++ .../tests/integration/test_issue_command.rs | 153 ++++ .../integration/test_subscribe_command.rs | 143 +++ e2etests/tests/mcp/mod.rs | 3 + e2etests/tests/mcp/test_mcp_command.rs | 78 ++ .../tests/mcp/test_mcp_command_regression.rs | 562 ++++++++++++ e2etests/tests/mcp/test_q_mcp_subcommand.rs | 353 ++++++++ e2etests/tests/model/mod.rs | 1 + .../tests/model/test_model_dynamic_command.rs | 187 ++++ e2etests/tests/q_subcommand/mod.rs | 13 + .../q_subcommand/test_q_chat_subcommand.rs | 29 + .../q_subcommand/test_q_debug_subcommand.rs | 205 +++++ .../q_subcommand/test_q_doctor_subcommand.rs | 27 + .../q_subcommand/test_q_inline_subcommand.rs | 305 +++++++ .../q_subcommand/test_q_quit_subcommand.rs | 31 + .../q_subcommand/test_q_restart_subcommand.rs | 25 + .../q_subcommand/test_q_setting_subcommand.rs | 102 +++ .../test_q_settings_deletecommand.rs | 44 + .../test_q_settings_format_command.rs | 26 + .../test_q_translate_subcommand.rs | 26 + .../q_subcommand/test_q_update_subcommand.rs | 50 + .../q_subcommand/test_q_user_subcommand.rs | 55 ++ .../q_subcommand/test_q_whoami_subcommand.rs | 133 +++ e2etests/tests/save_load/mod.rs | 1 + .../tests/save_load/test_save_load_command.rs | 444 +++++++++ e2etests/tests/session_mgmt/mod.rs | 3 + .../session_mgmt/test_compact_command.rs | 482 ++++++++++ .../tests/session_mgmt/test_usage_command.rs | 139 +++ e2etests/tests/todos/mod.rs | 1 + e2etests/tests/todos/test_todos_command.rs | 429 +++++++++ e2etests/tests/tools/mod.rs | 1 + e2etests/tests/tools/test_tools_command.rs | 689 ++++++++++++++ 60 files changed, 10437 insertions(+) create mode 100644 e2etests/.gitignore create mode 100644 e2etests/Cargo.lock create mode 100644 e2etests/Cargo.toml create mode 100644 e2etests/README.md create mode 100644 e2etests/analysis_report_template.html create mode 100644 e2etests/analyze_reports.py create mode 100644 e2etests/html_template.html create mode 100755 e2etests/run_tests.py create mode 100644 e2etests/src/lib.rs create mode 100644 e2etests/tests/agent/mod.rs create mode 100644 e2etests/tests/agent/test_agent_commands.rs create mode 100644 e2etests/tests/ai_prompts/mod.rs create mode 100644 e2etests/tests/ai_prompts/test_ai_prompt.rs create mode 100644 e2etests/tests/ai_prompts/test_prompts_commands.rs create mode 100644 e2etests/tests/all_tests.rs create mode 100644 e2etests/tests/context/mod.rs create mode 100644 e2etests/tests/context/test_context_command.rs create mode 100644 e2etests/tests/core_session/mod.rs create mode 100644 e2etests/tests/core_session/test_changelog_command.rs create mode 100644 e2etests/tests/core_session/test_clear_command.rs create mode 100644 e2etests/tests/core_session/test_command_introspect.rs create mode 100644 e2etests/tests/core_session/test_command_tangent.rs create mode 100644 e2etests/tests/core_session/test_help_command.rs create mode 100644 e2etests/tests/core_session/test_quit_command.rs create mode 100644 e2etests/tests/experiment/mod.rs create mode 100644 e2etests/tests/experiment/test_experiment_command.rs create mode 100644 e2etests/tests/integration/mod.rs create mode 100644 e2etests/tests/integration/test_editor_help_command.rs create mode 100644 e2etests/tests/integration/test_hooks_command.rs create mode 100644 e2etests/tests/integration/test_issue_command.rs create mode 100644 e2etests/tests/integration/test_subscribe_command.rs create mode 100644 e2etests/tests/mcp/mod.rs create mode 100644 e2etests/tests/mcp/test_mcp_command.rs create mode 100644 e2etests/tests/mcp/test_mcp_command_regression.rs create mode 100644 e2etests/tests/mcp/test_q_mcp_subcommand.rs create mode 100644 e2etests/tests/model/mod.rs create mode 100644 e2etests/tests/model/test_model_dynamic_command.rs create mode 100644 e2etests/tests/q_subcommand/mod.rs create mode 100644 e2etests/tests/q_subcommand/test_q_chat_subcommand.rs create mode 100644 e2etests/tests/q_subcommand/test_q_debug_subcommand.rs create mode 100644 e2etests/tests/q_subcommand/test_q_doctor_subcommand.rs create mode 100644 e2etests/tests/q_subcommand/test_q_inline_subcommand.rs create mode 100644 e2etests/tests/q_subcommand/test_q_quit_subcommand.rs create mode 100644 e2etests/tests/q_subcommand/test_q_restart_subcommand.rs create mode 100644 e2etests/tests/q_subcommand/test_q_setting_subcommand.rs create mode 100644 e2etests/tests/q_subcommand/test_q_settings_deletecommand.rs create mode 100644 e2etests/tests/q_subcommand/test_q_settings_format_command.rs create mode 100644 e2etests/tests/q_subcommand/test_q_translate_subcommand.rs create mode 100644 e2etests/tests/q_subcommand/test_q_update_subcommand.rs create mode 100644 e2etests/tests/q_subcommand/test_q_user_subcommand.rs create mode 100644 e2etests/tests/q_subcommand/test_q_whoami_subcommand.rs create mode 100644 e2etests/tests/save_load/mod.rs create mode 100644 e2etests/tests/save_load/test_save_load_command.rs create mode 100644 e2etests/tests/session_mgmt/mod.rs create mode 100644 e2etests/tests/session_mgmt/test_compact_command.rs create mode 100644 e2etests/tests/session_mgmt/test_usage_command.rs create mode 100644 e2etests/tests/todos/mod.rs create mode 100644 e2etests/tests/todos/test_todos_command.rs create mode 100644 e2etests/tests/tools/mod.rs create mode 100644 e2etests/tests/tools/test_tools_command.rs diff --git a/e2etests/.gitignore b/e2etests/.gitignore new file mode 100644 index 0000000000..f9344cfd40 --- /dev/null +++ b/e2etests/.gitignore @@ -0,0 +1,4 @@ +qcli_test_summary*.json +qcli_test_summary*.html +report-analysis/ +.amazonq/ \ No newline at end of file diff --git a/e2etests/Cargo.lock b/e2etests/Cargo.lock new file mode 100644 index 0000000000..6a810dc65e --- /dev/null +++ b/e2etests/Cargo.lock @@ -0,0 +1,202 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "conpty" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b72b06487a0d4683349ad74d62e87ad639b09667082b3c495c5b6bab7d84b3da" +dependencies = [ + "windows", +] + +[[package]] +name = "expectrl" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede784925953fcab9a3351d5009bcb8d2b0c13e940924c88087e8e2ce0c4717a" +dependencies = [ + "conpty", + "nix", + "ptyprocess", + "regex", +] + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags", + "cfg-if", + "libc", + "memoffset", + "pin-utils", +] + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "ptyprocess" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e05aef7befb11a210468a2d77d978dde2c6381a0381e33beb575e91f57fe8cf" +dependencies = [ + "nix", +] + +[[package]] +name = "q-cli-e2e-tests" +version = "0.1.0" +dependencies = [ + "expectrl", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "windows" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e745dab35a0c4c77aa3ce42d595e13d2003d6902d6b08c9ef5fc326d08da12b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" diff --git a/e2etests/Cargo.toml b/e2etests/Cargo.toml new file mode 100644 index 0000000000..395783e485 --- /dev/null +++ b/e2etests/Cargo.toml @@ -0,0 +1,49 @@ +[package] +name = "q-cli-e2e-tests" +version = "0.1.0" +edition = "2021" + +[workspace] + +[dependencies] +expectrl = "0.7" +regex = "1.0" +serial_test = "3.0" +ctor = "0.2" + + +[features] +core_session = ["help","tangent", "quit", "clear", "changelog","introspect"] # Core Session Commands (/help,/tangent, /quit, /clear, /changelog,introspect) +help = [] # Help Command (/help) +tangent = [] # Tangent Command (/tangent) +quit = [] # Quit Command (/quit) +clear = [] # Clear Command (/clear) +changelog = [] # Changelog Command (/changelog) +introspect = [] # Introspect Command (introspect) + +tools = [] # Tools Command (/tools) +agent = [] # Agent Commands (/agent list, /agent create, etc.) +context = [] # Context Commands (/context show, /context add, etc.) +save_load = [] # Save/Load Commands (/save, /load, help) +model = [] # Model Commands (/model, /model --help) + +session_mgmt = ["compact", "usage"] # Session Management Commands (/compact, /usage, help) +compact = [] # Compact Commands +usage = [] # Usage Commands + +integration = ["subscribe", "hooks", "editor", "issue_reporting"] # Integration Commands (/subscribe, /hooks, /editor, /issue help) +subscribe = [] # Subscribe Commands +hooks = [] # Hooks Commands +editor = [] # Editor Commands +issue_reporting = [] # Issue Reporting Commands + +mcp = [] # MCP Commands (/mcp, /mcp --help) +ai_prompts = [] # AI Prompts ("What is AWS?", "Hello") + +q_subcommand = [] # Q SubCommand (q chat, q doctor, q translate) + +todos = [] # todos command +experiment=[] # experiment command + +regression = [] # Regression Tests +sanity = [] # Sanity Tests - Quick smoke tests for basic functionality diff --git a/e2etests/README.md b/e2etests/README.md new file mode 100644 index 0000000000..a6e6cdd3ad --- /dev/null +++ b/e2etests/README.md @@ -0,0 +1,352 @@ +# Q CLI E2E Test Framework + +This test framework provides comprehensive end-to-end testing capabilities for Amazon Q CLI using a hybrid approach with expectrl and feature-based categorization. + +## ๐Ÿ—๏ธ Architecture + +### **Hybrid Approach** +- **expectrl (PTY)**: For interactive commands (`/help`, `/tools`, `/quit`, etc.) +- **Direct Process Streams**: For AI prompts ("What is AWS?", "Hello", etc.) + +### **Why Hybrid?** +- **Commands** write to PTY stream โ†’ expectrl captures perfectly โœ… +- **AI responses** write to stdout โ†’ direct streams capture properly โœ… + +## ๐ŸŽฏ Categorized Test Framework + +### **Feature-Based Organization** +Tests are organized into 12 functional categories using Rust features: + +1. **Agent Commands** (8 tests) - `agent` + - `/agent list`, `/agent create`, `/agent help`, etc. + +2. **AI Prompts** (5 tests) - `ai_prompts` + - "What is AWS?", "Hello" prompts + +3. **Context Commands** (10 tests) - `context` + - `/context show`, `/context add`, `/context help`, etc. + +4. **Core Session Commands** (3 tests) - `core_session` + - `/help`, `/quit`, `/clear` + +5. **Integration Commands** (21 tests) - `integration` + - `/subscribe`, `/hooks`, `/editor` help commands + +6. **MCP Commands** (18 tests) - `mcp` + - `/mcp`, `/mcp --help` + +7. **Model Commands** (3 tests) - `model` + - `/model`, `/model --help` + +8. **Q Subcommands** (15 tests) - `q_subcommand` + - q chat, q debug, q doctor, etc. + +9. **Save/Load Commands** (10 tests) - `save_load` + - `/save`, `/load`, help commands + +10. **Session Management Commands** (14 tests) - `session_mgmt` + - `/compact`, `/usage`, help commands + +11. **Todos Commands** - `todos` + - todos command + +12. **Tools Commands** (15 tests) - `tools` + - `/tools`, tool management commands + +## ๐Ÿ“ Core Files + +### **`src/lib.rs`** +Base helper class providing: +- `QChatSession::new()` - Start Q Chat session with timeout configuration +- `execute_command(cmd)` - Execute commands using expectrl + carriage return (`0x0D`) with character-by-character typing +- `send_prompt(prompt)` - Send AI prompts using direct process streams with concurrent stdout/stderr reading +- `send_key_input(key)` - Send key inputs for interactive navigation +- `execute_q_subcommand()` - Execute Q CLI subcommands directly in terminal +- `execute_q_subcommand_with_stdin()` - Execute Q CLI subcommands with stdin input support +- `execute_interactive_menu_selection()` - Handle interactive menu navigation with arrow keys and selection +- `execute_interactive_menu_selection_with_command()` - Execute interactive menu with full command string +- `read_response()` - Internal method for reading session responses with timeout handling +- `quit()` - Clean session termination + +### **`tests/all_tests.rs`** +Main test entry point that includes all test modules and organizes them by feature categories. + +### **`tests/*/mod.rs`** +Module files in test subdirectories that group related test functions by feature area. + +### **`run_tests.py`** +Python test runner with: +- **Real-time per-test feedback** - Shows โœ…/โŒ as each test completes +- **Category organization** - Groups tests by functional area +- **Configurable categories** - Enable/disable categories for faster iteration +- **Quiet and verbose modes** - Control output detail level +- **Final summary reporting** - Shows passed/failed categories +- **HTML and JSON reports** - Generates detailed test reports in both formats +- **Run complete test suites** - Execute all available test categories +- **Run individual features** - Target specific test categories +- **Check list of available features** - Display all available test categories +- **Convert JSON report to HTML** - Transform JSON reports into HTML format +- **Custom binary support** - Specify custom Q CLI binary path, defaults to system-installed `q` if not provided + +**Example Commands:** +```bash + +# Run individual features +python run_tests.py --features agent,context --quiet + +# Run default sanity test suite +python run_tests.py --quiet + +# List available features +python run_tests.py --list-features + +# Convert JSON to HTML report +python run_tests.py --json-to-html reports/test_report.json +``` + +## ๐Ÿš€ Usage + +### **Python Test Runner (Recommended)** + +```bash +# Run all categories with real-time feedback +python run_tests.py --quiet + +# Check help section for all available options +python run_tests.py --help + +``` + +**Example Output:** +``` +๐Ÿงช Running Sanity Test Suite +======================================== +๐Ÿ”„ Running: usage with sanity +โœ… usage (sanity) - 59.41s - 3 passed, 0 failed + +๐Ÿ“‹ Feature Summary: + โœ… usage (sanity): 3 passed, 0 failed + โœ… session_mgmt::test_usage_command::test_usage_command ... + โœ… session_mgmt::test_usage_command::test_usage_h_command ... + โœ… session_mgmt::test_usage_command::test_usage_help_command ... + +๐ŸŽฏ FINAL SUMMARY +================================ +๐Ÿท๏ธ Features Tested: 1 +โœ… Features 100% Pass: 1 +โŒ Features with Failures: 0 +โœ… Individual Tests Passed: 3 +โŒ Individual Tests Failed: 0 +๐Ÿ“Š Total Individual Tests: 3 +๐Ÿ“ˆ Success Rate: 100.0% + +๐ŸŽ‰ All tests passed! + +๐Ÿ“„ Detailed report saved to: reports/qcli_test_summary_usage_sanity_091625223834.json +๐ŸŒ HTML report saved to: reports/qcli_test_summary_usage_sanity_091625223834.html +``` + +### **Help and Configuration** +Use the help command to see all available options: + +```bash +# View all available options and categories +python run_tests.py --help +``` + +``` + +e2etests % python run_tests.py --help + +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: + run_tests.py [options] # Run tests with default settings + run_tests.py --features # Run specific features + run_tests.py --list-features # List available features + run_tests.py --json-to-html # Convert JSON report to HTML (provide JSON file path) + +options: + -h, --help show this help message and exit + --list-features List all available features + --json-to-html JSON_PATH + Convert JSON report to HTML (provide JSON file path) + --features FEATURES Comma-separated list of features + --binary BINARY Path to Q CLI binary + --quiet Quiet mode + +Examples: + # Basic usage + run_tests.py # Run all tests with default sanity suite + run_tests.py --features usage # Run usage tests with default sanity suite + run_tests.py --features "usage,agent" # Run usage+agent tests with default sanity suite + + # Test suites + run_tests.py --features sanity # Run all tests with sanity suite + run_tests.py --features regression # Run all tests with regression suite + run_tests.py --features "usage,regression" # Run usage tests with regression suite + + + # Multiple features (different ways) + run_tests.py --features "usage,agent,context" # Comma-separated features with default sanity suite + run_tests.py --features usage --features agent # Multiple --features flags with default sanity suite + run_tests.py --features core_session # Run grouped feature (includes help,quit,clear) with default sanity suite + + # Binary and output options + run_tests.py --binary /path/to/q --features usage # Executes the usage tests on provided q-cli binary instead of installed + run_tests.py --quiet --features sanity # Executes the tests in quiet mode + + # Utility commands + run_tests.py --list-features # List all available features + run_tests.py --json-to-html report.json # Convert JSON report (previously generated by running test) to HTML + + # Advanced examples + run_tests.py --features "core_session,regression" --binary ./target/release/q + run_tests.py --features "agent,mcp,sanity" --quiet +``` + +### **Individual Category Testing Using Cargo** +```bash +# Test specific categories using cargo directly +cargo test --tests --features "core_session" -- --nocapture +cargo test --tests --features "agent" -- --nocapture +cargo test --tests --features "ai_prompts" -- --nocapture +``` + +## โœ… Comprehensive Test Coverage + +### **Commands Tested (122+ tests)** +- **Agent Commands** (8 tests): `/agent list`, `/agent create`, `/agent help`, etc. +- **AI Prompts** (5 tests): "What is AWS?", "Hello" prompts +- **Context Commands** (10 tests): `/context show`, `/context add`, `/context help`, etc. +- **Core Session** (3 tests): `/help`, `/quit`, `/clear` +- **Integration Commands** (21 tests): `/subscribe`, `/hooks`, `/editor` help commands +- **MCP Commands** (18 tests): `/mcp`, `/mcp --help` +- **Model Commands** (3 tests): `/model`, `/model --help` +- **Q Subcommands** (15 tests): q chat, q debug, q doctor, etc. +- **Save/Load Commands** (10 tests): `/save`, `/load`, help commands +- **Session Management** (14 tests): `/compact`, `/usage`, help commands +- **Todos Commands**: todos command +- **Tools Commands** (15 tests): `/tools`, tool management commands + +### **AI Prompts Tested** +- "What is AWS?" - Technical explanation with verification +- "Hello" - Basic greeting response + +### **Verification Includes** +- **Content verification**: Specific text and sections present +- **Real-time feedback**: Per-test pass/fail status + +## ๐ŸŽฏ Success Metrics + +- **122 Total Tests** across 12 functional categories โœ… +- **Real-time feedback** with per-test results โœ… +- **Categorized organization** for better reporting โœ… +- **Configurable execution** for faster iteration โœ… +- **Comprehensive coverage** of all Q CLI commands โœ… + +## ๐Ÿ”ง Integration with Workspace + +This E2E test framework is designed to work with the Q CLI workspace: + +- **Default binary**: Uses system `q` command (from PATH) +- **Workspace integration**: Can test the workspace build +- **CI/CD integration**: Currently blocked due to Q CLI authentication issues +- **Custom binary support**: Test different builds as needed + +## ๐Ÿ”ง Extending + +### **Adding New Tests** + +1. **Create test file** in `tests/` directory +2. **Add feature attribute** to categorize the test: + ```rust + /// Brief description of what the test does + /// More detailed description of verification steps and expected behavior + #[test] + #[cfg(all(feature = "category_name", feature = "sanity"))] + fn test_new_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing /new command... | Description: Tests the /new command to verify functionality and expected behavior"); + + let session = get_chat_session(); + let mut chat = session.lock().unwrap(); + + let response = chat.execute_command("/new")?; + + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify response content + assert!(response.contains("expected_text"), "Missing expected content"); + println!("โœ… Command executed successfully!"); + + Ok(()) + } + ``` +3. **Update category configuration** in `run_tests.py` if needed + +### **Adding New Categories** + +1. **Add feature** to `Cargo.toml`: + ```toml + [features] + core_session = ["help", "quit", "clear"] + help = [] + quit = [] + clear = [] + tools = [] + agent = [] + context = [] + save_load = [] + model = [] + session_mgmt = ["compact", "usage"] + compact = [] + usage = [] + integration = ["subscribe", "hooks", "editor", "issue_reporting"] + subscribe = [] + hooks = [] + editor = [] + issue_reporting = [] + mcp = [] + ai_prompts = [] + q_subcommand = [] + regression = [] + sanity = [] + ``` +**Python script will automatically pick the features from toml file. +### **Test Patterns** + +- **For Commands**: Use `execute_command()` method with expectrl +- **For AI Prompts**: Use `send_prompt()` method with direct streams +- **Always**: Print full output first, then verify content +- **Pattern**: Start session โ†’ Execute โ†’ Verify โ†’ Quit + +This framework provides comprehensive, categorized E2E testing for the Q CLI with real-time feedback and flexible execution options. diff --git a/e2etests/analysis_report_template.html b/e2etests/analysis_report_template.html new file mode 100644 index 0000000000..a21c6fd7e3 --- /dev/null +++ b/e2etests/analysis_report_template.html @@ -0,0 +1,857 @@ + + + + + + Amazon Q CLI Test Analysis Report + + + + +
+

Test Automation Performance Dashboard

+

Amazon Q Developer CLI - Quality Assurance Analytics

+

Generated on:

+ +

Key Performance Indicators (KPIs)

+

+ Critical performance metrics across all test executions, providing executive-level insights and benchmarks for comparison. +

+
+
+
0
+
Total Reports
+
Total number of test execution reports analyzed
+
+
+
0
+
Features Tracked
+
Number of unique features being tracked across all reports
+
+
+
0
+
Avg Duration (min)
+
Average execution time across all test runs
+
+
+
0
+
Best Time (min)
+
Fastest execution time recorded across all test runs
+
+
+
0
+
Best w/ Current Tests# (min)
+
Best execution time with test count โ‰ฅ latest execution
+
+
+
0
+
Latest Execution (min)
+
Execution time of the most recent test run
+
+
+
0
+
Avg Last 3 (min)
+
Average execution time of the last 3 test runs
+
+
+
0
+
Avg Last 5 (min)
+
Average execution time of the last 5 test runs
+
+
+
0%
+
Last 3 vs Overall Avg
+
Performance change: negative % = faster (good), positive % = slower (bad)
+
+
+ +

Performance Trend Analysis

+

+ Historical view of test execution duration and test count over time, helping identify performance trends and test suite growth. +

+
+ +
+ +

Feature Performance Tracking

+

+ Individual feature performance tracking over time. Select a feature to view its execution duration and test count history. +

+
+ +
+
+ +
+ +

Performance Baseline Comparison

+

+ This histogram compares the latest execution time of each feature against its average of the last 3 executions. + Green bars indicate features performing at or better than their recent average, while red bars show features + taking longer than their recent average performance. +

+
+
+
+ Duration โ‰ค Last 3 Avg (Good Performance) +
+
+
+ Duration > Last 3 Avg (Performance Regression) +
+
+
+ +
+ +

Multi-Metric Performance Analysis

+

+ This chart shows multiple performance metrics for each feature side by side. Latest execution uses red/green logic + (red if > last 3 avg, green otherwise), while other metrics use consistent colors for easy comparison. +

+
+
+
+
+
+
+ Latest Execution (Green: โ‰ค Last 3 Avg, Red: > Last 3 Avg) +
+
+
+ Overall Average +
+
+
+ Recent Average (Last 3) +
+
+
+ Extended Average (Last 5) +
+
+
+ +
+ +

Execution History & Analytics

+

+ Chronological summary of all test executions with duration comparisons against historical averages. Recent executions shown first. +

+
+ + + + + + + + + + + + +
DateTotal TestsTotal FailedDuration (minutes)Duration (hours)vs Average
+
+ +
+

Performance Metrics Analysis

+

+ Detailed performance metrics for each feature including latest execution time with color-coded performance indicators. +

+
+ + + + + + + + + + + + +
Feature โ†•Latest Time (min) โ†•Avg Duration (min) โ†•Avg Last 3 (min) โ†•Avg Last 5 (min) โ†•Best w/ Same Tests โ†•
+
+
+ +
+

Performance Change Analysis

+

+ Comprehensive analysis of execution time changes with comparisons against multiple baselines and feature-level breakdowns. +

+
+
+ +

Quality Assurance Matrix

+

+ Pass/fail status matrix showing feature reliability over time. Green indicates pass, red indicates failure, gray indicates not tested. +

+
+ + + + + + + +
Feature
+
+ +
+

Reliability & Risk Assessment

+

+ Statistical analysis of feature failure rates, helping identify the most problematic features requiring attention. +

+
+ + + + + + + + + + +
Feature โ†•Failure Rate (%) โ†•Total Runs โ†•Failed Runs โ†•
+
+
+
+ + + + \ No newline at end of file diff --git a/e2etests/analyze_reports.py b/e2etests/analyze_reports.py new file mode 100644 index 0000000000..b11f252a5f --- /dev/null +++ b/e2etests/analyze_reports.py @@ -0,0 +1,455 @@ +#!/usr/bin/env python3 +import json +import os +import sys +from datetime import datetime +from collections import defaultdict +import argparse + +def parse_filename_date(filename): + """Extract date from filename format: qcli_test_summary_sanity_MMDDYYhhmmss.json""" + try: + parts = filename.split('_') + if len(parts) >= 4: + date_str = parts[-1].replace('.json', '') + if len(date_str) == 12: # MMDDYYhhmmss + month = date_str[:2] + day = date_str[2:4] + year = '20' + date_str[4:6] + hour = date_str[6:8] + minute = date_str[8:10] + second = date_str[10:12] + return datetime.strptime(f"{year}-{month}-{day} {hour}:{minute}:{second}", "%Y-%m-%d %H:%M:%S") + except: + pass + return None + +def analyze_reports(directory_path): + """Analyze all JSON reports in the directory""" + reports_data = [] + + for filename in os.listdir(directory_path): + if filename.endswith('.json') and 'qcli_test_summary' in filename: + filepath = os.path.join(directory_path, filename) + + try: + with open(filepath, 'r') as f: + data = json.load(f) + + # Extract date from filename + file_date = parse_filename_date(filename) + if not file_date: + continue + + # Calculate actual total duration from detailed_results + total_tests = data.get('summary', {}).get('total_individual_tests', 0) + duration = 0 + for result in data.get('detailed_results', []): + duration += result.get('duration', 0) + + # Extract Q version from system_info + q_version = data.get('system_info', {}).get('q_version', 'Unknown') + + report_info = { + 'filename': filename, + 'date': file_date, + 'total_tests': total_tests, + 'passed': data.get('summary', {}).get('passed', 0), + 'failed': data.get('summary', {}).get('failed', 0), + 'success_rate': data.get('summary', {}).get('success_rate', 0), + 'duration': duration, + 'q_version': q_version, + 'features': {} + } + + # Extract feature data with actual durations + for feature_name, feature_data in data.get('features', {}).items(): + # Find actual duration for this feature from detailed_results + feature_duration = 0 + for result in data.get('detailed_results', []): + if result.get('feature') == feature_name: + feature_duration = result.get('duration', 0) + break + + report_info['features'][feature_name] = { + 'passed': feature_data.get('passed', 0), + 'failed': feature_data.get('failed', 0), + 'total': feature_data.get('passed', 0) + feature_data.get('failed', 0), + 'status': 'Pass' if feature_data.get('failed', 0) == 0 else 'Fail', + 'duration': feature_duration + } + + reports_data.append(report_info) + + except Exception as e: + print(f"Error processing {filename}: {e}") + + # Sort by date + reports_data.sort(key=lambda x: x['date']) + return reports_data + +def generate_analytics(reports_data): + """Generate analytical insights""" + if not reports_data: + return {} + + # Feature failure analysis + feature_failures = defaultdict(int) + feature_totals = defaultdict(int) + all_features = set() + + for report in reports_data: + for feature_name, feature_data in report['features'].items(): + all_features.add(feature_name) + feature_totals[feature_name] += 1 + if feature_data['status'] == 'Fail': + feature_failures[feature_name] += 1 + + # Calculate failure rates and analysis + feature_failure_rates = {} + feature_failure_analysis = {} + for feature in all_features: + total_runs = feature_totals[feature] + failed_runs = feature_failures[feature] + failure_rate = (failed_runs / total_runs) * 100 if total_runs > 0 else 0 + + feature_failure_rates[feature] = failure_rate + feature_failure_analysis[feature] = { + 'failure_rate': failure_rate, + 'total_runs': total_runs, + 'failed_runs': failed_runs + } + + # Feature analytics with duration and max test count (calculate first) + feature_analytics = {} + for feature in all_features: + total_duration = 0 + count = 0 + max_tests = 0 + min_duration = float('inf') + min_duration_test_count = 0 + latest_test_count = 0 + latest_duration = 0 + feature_durations = [] + + # Collect all durations for this feature in chronological order + for report in reports_data: + if feature in report['features']: + if 'duration' in report['features'][feature]: + duration = report['features'][feature]['duration'] + test_count = report['features'][feature]['total'] + feature_durations.append(duration) + total_duration += duration + count += 1 + if duration < min_duration: + min_duration = duration + min_duration_test_count = test_count + max_tests = max(max_tests, report['features'][feature]['total']) + + # Find latest values from most recent report + for report in reversed(reports_data): + if feature in report['features']: + latest_test_count = report['features'][feature]['total'] + latest_duration = report['features'][feature].get('duration', 0) + break + + # Find best time with test count >= latest test count + best_duration_with_tests = float('inf') + best_test_count = 0 + for report in reports_data: + if feature in report['features']: + test_count = report['features'][feature]['total'] + duration = report['features'][feature].get('duration', 0) + if test_count >= latest_test_count and duration > 0 and duration < best_duration_with_tests: + best_duration_with_tests = duration + best_test_count = test_count + + # Calculate rolling averages + avg_last_3 = 0 + avg_last_5 = 0 + if len(feature_durations) >= 3: + avg_last_3 = sum(feature_durations[-3:]) / 3 + if len(feature_durations) >= 5: + avg_last_5 = sum(feature_durations[-5:]) / 5 + + feature_analytics[feature] = { + 'avg_duration': round((total_duration / count) / 60, 2) if count > 0 else 0, + 'best_duration': f"{round(min_duration / 60, 2)} ({min_duration_test_count})" if min_duration != float('inf') else "N/A", + 'best_duration_with_tests': f"{round(best_duration_with_tests / 60, 2)} ({best_test_count})" if best_duration_with_tests != float('inf') else "N/A", + 'max_tests': max_tests, + 'latest_test_count': latest_test_count, + 'latest_duration': round(latest_duration / 60, 2), + 'avg_last_3': round(avg_last_3 / 60, 2) if avg_last_3 > 0 else 0, + 'avg_last_5': round(avg_last_5 / 60, 2) if avg_last_5 > 0 else 0 + } + + # Duration analysis with feature breakdown (include all tests) + duration_changes = [] + for i in range(1, len(reports_data)): + prev_duration = reports_data[i-1]['duration'] + curr_duration = reports_data[i]['duration'] + change = ((curr_duration - prev_duration) / prev_duration) * 100 if prev_duration > 0 else 0 + + # Calculate change vs previous 3 and 5 executions average + change_vs_prev_3 = 0 + prev_3_avg = 0 + if i >= 3: + prev_3_avg = sum(reports_data[j]['duration'] for j in range(i-3, i)) / 3 + change_vs_prev_3 = ((curr_duration - prev_3_avg) / prev_3_avg) * 100 if prev_3_avg > 0 else 0 + + change_vs_prev_5 = 0 + prev_5_avg = 0 + if i >= 5: + prev_5_avg = sum(reports_data[j]['duration'] for j in range(i-5, i)) / 5 + change_vs_prev_5 = ((curr_duration - prev_5_avg) / prev_5_avg) * 100 if prev_5_avg > 0 else 0 + + # Calculate comparisons with historical data only (up to current date) + historical_reports = reports_data[:i+1] # Only include reports up to current index + overall_avg = sum(report['duration'] for report in historical_reports) / len(historical_reports) + best_time = min(report['duration'] for report in historical_reports) + + # Find best time with current test count from historical data only + current_test_count = reports_data[i]['total_tests'] + best_with_current = float('inf') + for report in historical_reports: + if report['total_tests'] >= current_test_count: + best_with_current = min(best_with_current, report['duration']) + best_with_current = best_with_current if best_with_current != float('inf') else curr_duration + + change_vs_avg = ((curr_duration - overall_avg) / overall_avg) * 100 if overall_avg > 0 else 0 + change_vs_best = ((curr_duration - best_time) / best_time) * 100 if best_time > 0 else 0 + change_vs_best_current = ((curr_duration - best_with_current) / best_with_current) * 100 if best_with_current > 0 else 0 + + # Calculate feature breakdown with test count changes + feature_breakdown = [] + for feature in all_features: + curr_feat_dur = 0 + curr_test_count = 0 + + if feature in reports_data[i]['features']: + curr_feat_dur = reports_data[i]['features'][feature].get('duration', 0) / 60 + curr_test_count = reports_data[i]['features'][feature].get('total', 0) + + # Find nearest previous execution of this feature + prev_test_count = 0 + for j in range(i-1, -1, -1): # Go backwards from current report + if feature in reports_data[j]['features']: + prev_test_count = reports_data[j]['features'][feature].get('total', 0) + break + + avg_feat_dur = feature_analytics.get(feature, {}).get('avg_duration', 0) + test_change = curr_test_count - prev_test_count + + feature_breakdown.append({ + 'feature': feature, + 'current_duration': round(curr_feat_dur, 2), + 'average_duration': avg_feat_dur, + 'current_test_count': curr_test_count, + 'previous_test_count': prev_test_count, + 'test_change': test_change + }) + + duration_changes.append({ + 'date': reports_data[i]['date'], + 'change_percent': change, + 'change_vs_prev_3': change_vs_prev_3, + 'change_vs_prev_5': change_vs_prev_5, + 'change_vs_avg': change_vs_avg, + 'change_vs_best': change_vs_best, + 'change_vs_best_current': change_vs_best_current, + 'prev_duration_minutes': round(prev_duration / 60, 2), + 'prev_3_avg_minutes': round(prev_3_avg / 60, 2) if prev_3_avg > 0 else 0, + 'prev_5_avg_minutes': round(prev_5_avg / 60, 2) if prev_5_avg > 0 else 0, + 'overall_avg_minutes': round(overall_avg / 60, 2), + 'best_time_minutes': round(best_time / 60, 2), + 'best_current_minutes': round(best_with_current / 60, 2), + 'curr_duration_minutes': round(curr_duration / 60, 2), + 'total_tests': reports_data[i]['total_tests'], + 'q_version': reports_data[i]['q_version'], + 'significant_test': f"Duration changed by {change:+.1f}%", + 'feature_breakdown': feature_breakdown + }) + + # Sort by date descending (most recent first) + duration_changes.sort(key=lambda x: x['date'], reverse=True) + + # Keep backward compatibility + feature_avg_duration = {k: v['avg_duration'] for k, v in feature_analytics.items()} + + return { + 'feature_failure_rates': dict(sorted(feature_failure_rates.items(), key=lambda x: x[1], reverse=True)), + 'feature_failure_analysis': feature_failure_analysis, + 'all_duration_changes': duration_changes, + 'feature_avg_duration': feature_avg_duration, + 'feature_analytics': feature_analytics, + 'all_features': sorted(all_features), + 'total_reports': len(reports_data) + } + +def generate_html_report(reports_data, analytics, output_file): + """Generate HTML report""" + + # Prepare data for charts (convert actual duration to minutes) + dates = [report['date'].strftime('%m/%d/%y') for report in reports_data] + durations = [round(report['duration'] / 60, 2) for report in reports_data] # Actual duration in minutes + test_counts = [report['total_tests'] for report in reports_data] + + # Calculate overall execution statistics + avg_duration_minutes = sum(durations) / len(durations) if durations else 0 + best_overall_time = min(durations) if durations else 0 + latest_execution_time = durations[-1] if durations else 0 + + # Calculate best time with latest test count + latest_test_count = reports_data[-1]['total_tests'] if reports_data else 0 + best_with_current_tests = float('inf') + for i, report in enumerate(reports_data): + if report['total_tests'] >= latest_test_count: + best_with_current_tests = min(best_with_current_tests, durations[i]) + best_with_current_tests = best_with_current_tests if best_with_current_tests != float('inf') else 0 + + # Calculate rolling averages + avg_last_3 = sum(durations[-3:]) / len(durations[-3:]) if len(durations) >= 3 else 0 + avg_last_5 = sum(durations[-5:]) / len(durations[-5:]) if len(durations) >= 5 else 0 + + # Add overall stats to analytics + overall_stats = { + 'avg_duration': round(avg_duration_minutes, 2), + 'best_time': round(best_overall_time, 2), + 'best_with_current_tests': round(best_with_current_tests, 2), + 'latest_execution': round(latest_execution_time, 2), + 'avg_last_3': round(avg_last_3, 2), + 'avg_last_5': round(avg_last_5, 2) + } + + # Prepare test summary data with average comparison + test_summary = [] + for report in reports_data: + duration_minutes = round(report['duration'] / 60, 2) + diff_from_avg = ((duration_minutes - avg_duration_minutes) / avg_duration_minutes) * 100 if avg_duration_minutes > 0 else 0 + is_significant = abs(diff_from_avg) > 20 # 20% threshold + + test_summary.append({ + 'date': report['date'].strftime('%m/%d/%y'), + 'total_tests': report['total_tests'], + 'total_failed': report['failed'], + 'duration_minutes': duration_minutes, + 'duration_hours': round(report['duration'] / 3600, 2), + 'avg_comparison': round(diff_from_avg, 1), + 'is_significant': is_significant + }) + + # Feature matrix data + feature_matrix = [] + for feature in analytics['all_features']: + row = {'feature': feature} + for report in reports_data: + date_key = report['date'].strftime('%m/%d/%y') + if feature in report['features']: + row[date_key] = report['features'][feature]['status'] + else: + row[date_key] = 'N/A' + feature_matrix.append(row) + + with open('analysis_report_template.html', 'r') as f: + template = f.read() + + # Prepare feature-wise trends data + feature_trends = {} + for feature in analytics['all_features']: + feature_durations = [] + feature_test_counts = [] + for report in reports_data: + if feature in report['features']: + feature_durations.append(round(report['features'][feature].get('duration', 0) / 60, 2)) + feature_test_counts.append(report['features'][feature]['total']) + else: + feature_durations.append(0) + feature_test_counts.append(0) + feature_trends[feature] = { + 'durations': feature_durations, + 'test_counts': feature_test_counts + } + + # Prepare feature histogram data + feature_histogram_data = { + 'labels': [], + 'values': [], + 'colors': [] + } + + for feature in sorted(analytics['all_features']): + if feature in analytics['feature_analytics']: + data = analytics['feature_analytics'][feature] + latest_duration = data['latest_duration'] + avg_last_3 = data['avg_last_3'] + + feature_histogram_data['labels'].append(feature) + feature_histogram_data['values'].append(latest_duration) + + # Color coding: red if duration higher than last 3 average, else green + if avg_last_3 > 0 and latest_duration > avg_last_3: + feature_histogram_data['colors'].append('rgba(220, 53, 69, 0.8)') # Red + else: + feature_histogram_data['colors'].append('rgba(40, 167, 69, 0.8)') # Green + + # Replace placeholders + html_content = template.replace('{{DATES}}', str(dates)) + html_content = html_content.replace('{{DURATIONS}}', str(durations)) + html_content = html_content.replace('{{TEST_COUNTS}}', str(test_counts)) + html_content = html_content.replace('{{FEATURE_MATRIX}}', json.dumps(feature_matrix)) + # Add overall stats to analytics + analytics['overall_stats'] = overall_stats + html_content = html_content.replace('{{ANALYTICS}}', json.dumps(analytics, default=str)) + html_content = html_content.replace('{{TEST_SUMMARY}}', json.dumps(test_summary)) + html_content = html_content.replace('{{FEATURE_TRENDS}}', json.dumps(feature_trends)) + html_content = html_content.replace('{{FEATURE_HISTOGRAM_DATA}}', json.dumps(feature_histogram_data)) + + with open(output_file, 'w') as f: + f.write(html_content) + +def main(): + parser = argparse.ArgumentParser(description='Analyze Amazon Q CLI test reports') + parser.add_argument('directory', help='Directory containing JSON report files') + parser.add_argument('-o', '--output', help='Output HTML file (default: timestamped file in report-analysis/)') + + args = parser.parse_args() + + # Generate timestamped filename if not provided + if not args.output: + timestamp = datetime.now().strftime('%m%d%y%H%M%S') + args.output = f'report-analysis/test_analysis_report_{timestamp}.html' + + if not os.path.isdir(args.directory): + print(f"Error: Directory '{args.directory}' does not exist") + sys.exit(1) + + print("Analyzing test reports...") + reports_data = analyze_reports(args.directory) + + if not reports_data: + print("No valid test reports found") + sys.exit(1) + + print(f"Found {len(reports_data)} reports") + + analytics = generate_analytics(reports_data) + + print("Generating HTML report...") + generate_html_report(reports_data, analytics, args.output) + + print(f"Report generated: {args.output}") + + # Print summary + print("\n=== SUMMARY ===") + print(f"Total Reports: {analytics['total_reports']}") + print(f"Features with highest failure rates:") + for feature, rate in list(analytics['feature_failure_rates'].items())[:5]: + print(f" {feature}: {rate:.1f}%") + + if analytics['all_duration_changes']: + print(f"\nRecent duration changes:") + for change in analytics['all_duration_changes'][:3]: # Show first 3 (most recent) + print(f" {change['date'].strftime('%m/%d/%y')}: {change['change_percent']:+.1f}% ({change['curr_duration_minutes']} min)") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/e2etests/html_template.html b/e2etests/html_template.html new file mode 100644 index 0000000000..d61f10f7ae --- /dev/null +++ b/e2etests/html_template.html @@ -0,0 +1,258 @@ + + + + + + Q CLI Test Report + + + +
+
+

๐Ÿงช Q CLI E2E Test Report

+

Generated: {timestamp}

+
+ +
+

๐Ÿ“Š Summary

+
+ +
+
+
+ Total Tests +
+
+
+ Passed Tests +
+
+
+
+
+
+
+

Features Tested

+

{total_features}

+
+
+

Tests Passed

+

{tests_passed}

+
+
+

Tests Failed

+

{tests_failed}

+
+
+
+
+
+ + {test_suites_content} + +
+

๐Ÿ’ป System Information

+

Platform: {platform}

+ +

Q Binary: {q_binary_info}

+
+
+ + + + \ No newline at end of file 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 new file mode 100644 index 0000000000..bcbed0616b --- /dev/null +++ b/e2etests/src/lib.rs @@ -0,0 +1,262 @@ +pub mod q_chat_helper { + //! Helper module for Q CLI testing with hybrid approach + //! - expectrl for commands (/help, /tools) + //! - Direct process streams for AI prompts + + pub use expectrl::{Regex, Error}; + pub use std::io::{Read, Write}; + pub use std::time::Duration; + pub use std::process::{Command, Stdio}; + pub use std::thread; + pub use std::sync::{Mutex, OnceLock}; + + static GLOBAL_CHAT_SESSION: OnceLock> = OnceLock::new(); + + pub struct QChatSession { + session: expectrl::Session, + } + + impl QChatSession { + /// Start a new Q Chat session + pub fn new() -> Result { + let q_binary = std::env::var("Q_CLI_PATH").unwrap_or_else(|_| "q".to_string()); + let command = format!("{} chat", q_binary); + let mut session = expectrl::spawn(&command)?; + session.set_expect_timeout(Some(Duration::from_secs(60))); + + // Wait for startup prompt + session.expect(Regex(r">"))?; + + Ok(QChatSession { session }) + } + + /// Execute a command (like /help, /tools) and return the response + pub fn execute_command(&mut self, command: &str) -> Result { + self.execute_command_with_timeout(command, None) + } + + /// Execute a command with custom timeout + pub fn execute_command_with_timeout(&mut self, command: &str, timeout_ms: Option) -> Result { + // Type command character by character with delays (for autocomplete) + for &byte in command.as_bytes() { + self.session.write_all(&[byte])?; + self.session.flush()?; + std::thread::sleep(Duration::from_millis(50)); + } + + // Send carriage return to execute + self.session.write_all(&[0x0D])?; + self.session.flush()?; + + self.read_response(timeout_ms) + } + + /// Send a regular chat prompt (like "What is AWS?") and return the response + pub fn send_prompt(&mut self, prompt: &str) -> Result { + // For AI prompts, we need to use direct process streams to capture stdout + self.session.send_line("/quit")?; // Close current session + + // Start new process with direct stream access + let q_binary = std::env::var("Q_CLI_PATH").unwrap_or_else(|_| "q".to_string()); + let mut child = Command::new(&q_binary) + .arg("chat") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .map_err(|e| Error::IO(e))?; + + let mut stdin = child.stdin.take().unwrap(); + let stdout = child.stdout.take().unwrap(); + let stderr = child.stderr.take().unwrap(); + + // Send the prompt + writeln!(stdin, "{}", prompt).map_err(|e| Error::IO(e))?; + drop(stdin); + + // Read both stdout and stderr concurrently + let stdout_handle = std::thread::spawn(move || { + let mut content = String::new(); + let mut stdout = stdout; + let _ = stdout.read_to_string(&mut content); + content + }); + + let stderr_handle = std::thread::spawn(move || { + let mut content = String::new(); + let mut stderr = stderr; + let _ = stderr.read_to_string(&mut content); + content + }); + + let stdout_content = stdout_handle.join().unwrap_or_default(); + let stderr_content = stderr_handle.join().unwrap_or_default(); + + // Wait for process to complete + let _ = child.wait(); + + // Combine stderr (UI elements) and stdout (AI response) + let combined = format!("{}{}", stderr_content, stdout_content); + Ok(combined) + } + + fn read_response(&mut self, timeout_ms: Option) -> Result { + let timeout = timeout_ms.unwrap_or(6000); + let mut total_content = String::new(); + + for _ in 0..15 { + let mut buffer = [0u8; 512]; + match self.session.try_read(&mut buffer) { + Ok(bytes_read) if bytes_read > 0 => { + let chunk = String::from_utf8_lossy(&buffer[..bytes_read]); + total_content.push_str(&chunk); + }, + Ok(_) => { + // No more data, but wait a bit more in case there's more coming + std::thread::sleep(Duration::from_millis(timeout)); + if total_content.len() > 0 { break; } + }, + Err(_) => break, + } + std::thread::sleep(Duration::from_millis(timeout)); + } + + Ok(total_content) + } + + /// Send key input (like arrow keys, Enter, etc.) + pub fn send_key_input(&mut self, key_sequence: &str) -> Result { + self.send_key_input_with_timeout(key_sequence, None) + } + + /// Send key input with custom timeout + pub fn send_key_input_with_timeout(&mut self, key_sequence: &str, timeout_ms: Option) -> Result { + self.session.write_all(key_sequence.as_bytes())?; + self.session.flush()?; + std::thread::sleep(Duration::from_millis(200)); + self.read_response(timeout_ms) + } + + /// Quit the Q Chat session + pub fn quit(&mut self) -> Result<(), Error> { + self.session.send_line("/quit")?; + 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) + } + + /// Get or create the global shared chat session + pub fn get_chat_session() -> &'static Mutex { + GLOBAL_CHAT_SESSION.get_or_init(|| { + let chat = QChatSession::new().expect("Failed to create chat session"); + println!("โœ… Global Q Chat session started"); + Mutex::new(chat) + }) + } + + /// Create a new isolated chat session (not shared) + pub fn get_new_chat_session() -> Result, Error> { + let chat = QChatSession::new()?; + println!("โœ… New isolated Q Chat session created"); + Ok(Mutex::new(chat)) + } + + /// Close the global chat session + pub fn close_session() -> Result<(), Error> { + if let Some(session) = GLOBAL_CHAT_SESSION.get() { + if let Ok(mut chat) = session.lock() { + chat.quit()?; + println!("โœ… Global chat session closed"); + } + } + Ok(()) + } + +} 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/agent/test_agent_commands.rs b/e2etests/tests/agent/test_agent_commands.rs new file mode 100644 index 0000000000..ec18e19996 --- /dev/null +++ b/e2etests/tests/agent/test_agent_commands.rs @@ -0,0 +1,556 @@ +#[allow(unused_imports)] +use q_cli_e2e_tests::q_chat_helper; + +// Tests the /agent command without subcommands to display help information +//Verifies agent management description, usage, available subcommands, and options +#[test] +#[cfg(all(feature = "agent", feature = "sanity"))] +fn agent_without_subcommand() -> Result<(), Box> { + 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 = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("/agent",Some(1000))?; + + println!("๐Ÿ“ Agent response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + assert!(response.contains("Manage agents"), "Missing 'Manage agents' description"); + assert!(response.contains("Usage:"), "Missing usage information"); + assert!(response.contains("/agent"), "Missing agent command"); + assert!(response.contains(""), "Missing command placeholder"); + println!("โœ… Found agent command description and usage"); + + assert!(response.contains("Commands:"), "Missing Commands section"); + assert!(response.contains("list"), "Missing list subcommand"); + assert!(response.contains("create"), "Missing create subcommand"); + assert!(response.contains("schema"), "Missing schema subcommand"); + assert!(response.contains("set-default"), "Missing set-default subcommand"); + assert!(response.contains("help"), "Missing help subcommand"); + println!("โœ… Verified all agent subcommands: list, create, schema, set-default, help"); + + assert!(response.contains("List all available agents"), "Missing list command description"); + assert!(response.contains("Create a new agent"), "Missing create command description"); + assert!(response.contains("Show agent config schema"), "Missing schema command description"); + assert!(response.contains("Define a default agent"), "Missing set-default command description"); + println!("โœ… Verified command descriptions"); + + assert!(response.contains("Options:"), "Missing Options section"); + assert!(response.contains("-h"), "Missing short help option"); + assert!(response.contains("--help"), "Missing long help option"); + println!("โœ… Found options section with help flag"); + + println!("โœ… /agent command executed successfully"); + + // Release the lock before cleanup + drop(chat); + + 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(all(feature = "agent", feature = "sanity"))] +fn test_agent_create_command() -> Result<(), Box> { + 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) + .unwrap() + .as_secs(); + let agent_name = format!("test_demo_agent_{}", timestamp); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let create_response = chat.execute_command_with_timeout(&format!("/agent create --name {}", agent_name),Some(1000))?; + + println!("๐Ÿ“ Agent create response: {} bytes", create_response.len()); + println!("๐Ÿ“ CREATE RESPONSE:"); + println!("{}", create_response); + println!("๐Ÿ“ END CREATE RESPONSE"); + + let save_response = chat.execute_command(":wq")?; + + println!("๐Ÿ“ Save response: {} bytes", save_response.len()); + println!("๐Ÿ“ SAVE RESPONSE:"); + println!("{}", save_response); + println!("๐Ÿ“ END SAVE RESPONSE"); + + assert!(save_response.contains("Agent") && save_response.contains(&agent_name) && save_response.contains("has been created successfully"), "Missing agent creation success message"); + println!("โœ… Found agent creation success message"); + + let whoami_response = chat.execute_command_with_timeout("!whoami",Some(1000))?; + + println!("๐Ÿ“ Whoami response: {} bytes", whoami_response.len()); + println!("๐Ÿ“ WHOAMI RESPONSE:"); + println!("{}", whoami_response); + println!("๐Ÿ“ END WHOAMI RESPONSE"); + + let lines: Vec<&str> = whoami_response.lines().collect(); + let username = lines.iter() + .find(|line| !line.starts_with("!") && !line.starts_with(">") && !line.trim().is_empty()) + .expect("Failed to get username from whoami command") + .trim(); + println!("โœ… Current username: {}", username); + + let agent_path = format!("/Users/{}/.aws/amazonq/cli-agents/{}.json", username, agent_name); + println!("โœ… Agent path: {}", agent_path); + + if std::path::Path::new(&agent_path).exists() { + std::fs::remove_file(&agent_path)?; + println!("โœ… Agent file deleted: {}", agent_path); + } else { + println!("โš ๏ธ Agent file not found at: {}", agent_path); + } + + assert!(!std::path::Path::new(&agent_path).exists(), "Agent file should be deleted"); + println!("โœ… Agent deletion verified"); + + // Release the lock before cleanup + drop(chat); + + Ok(()) +} + +/// Tests the /agent edit command to edit a existing agent with specified name +/// Verifies agent edit process, file system operations, and cleanup +#[test] +#[cfg(all(feature = "agent", feature = "sanity"))] +fn test_agent_edit_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing /agent edit --name command... | Description: Tests the /agent edit command to edit a existing agent. Verifies agent edit process, file system operations, and cleanup"); + + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + let agent_name = format!("test_demo_agent_{}", timestamp); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + chat.execute_command_with_timeout(&format!("/agent create --name {}", agent_name),Some(1000))?; + + let save_response = chat.execute_command(":wq")?; + + + assert!(save_response.contains("Agent") && save_response.contains(&agent_name) && save_response.contains("has been created successfully"), "Missing agent creation success message"); + println!("โœ… Found agent creation success message"); + + // Edit the agent description + let edit_response = chat.execute_command_with_timeout(&format!("/agent edit --name {}", agent_name),Some(2000))?; + + println!("๐Ÿ“ Agent edit response: {} bytes", edit_response.len()); + println!("๐Ÿ“ EDIT RESPONSE:"); + println!("{}", edit_response); + println!("๐Ÿ“ END EDIT RESPONSE"); + + + // Use line-based editing + chat.execute_command("3G")?; // Go to line 2 (assuming description is there) + chat.execute_command("S")?; // Delete line and enter insert mode + chat.execute_command(" \"description\": \"Updated agent description for testing\",")?; + chat.execute_command("\u{1b}")?; // ESC + + let save_edit = chat.execute_command(":wq")?; + + println!("๐Ÿ“ Edit save response: {} bytes", save_edit.len()); + println!("๐Ÿ“ EDIT SAVE RESPONSE:"); + println!("{}", save_edit); + println!("๐Ÿ“ END EDIT SAVE RESPONSE"); + + assert!(save_edit.contains("Agent") && save_edit.contains(&agent_name) && save_edit.contains("has been edited successfully"), "Missing agent update success message"); + println!("โœ… Found agent update success message"); + + let whoami_response = chat.execute_command_with_timeout("!whoami",Some(500))?; + + println!("๐Ÿ“ Whoami response: {} bytes", whoami_response.len()); + println!("๐Ÿ“ WHOAMI RESPONSE:"); + println!("{}", whoami_response); + println!("๐Ÿ“ END WHOAMI RESPONSE"); + + let lines: Vec<&str> = whoami_response.lines().collect(); + let username = lines.iter() + .find(|line| !line.starts_with("!") && !line.starts_with(">") && !line.trim().is_empty()) + .expect("Failed to get username from whoami command") + .trim(); + println!("โœ… Current username: {}", username); + + let agent_path = format!("/Users/{}/.aws/amazonq/cli-agents/{}.json", username, agent_name); + println!("โœ… Agent path: {}", agent_path); + + if std::path::Path::new(&agent_path).exists() { + std::fs::remove_file(&agent_path)?; + println!("โœ… Agent file deleted: {}", agent_path); + } else { + println!("โš ๏ธ Agent file not found at: {}", agent_path); + } + + assert!(!std::path::Path::new(&agent_path).exists(), "Agent file should be deleted"); + println!("โœ… Agent deletion verified"); + + //Release the lock before cleanup + drop(chat); + + Ok(()) +} +/// Tests the /agent create command without required arguments to verify error handling +/// Verifies proper error messages, usage information, and help suggestions +#[test] +#[cfg(all(feature = "agent", feature = "sanity"))] +fn test_agent_create_missing_args() -> Result<(), Box> { + 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 = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("/agent create",Some(2000))?; + + println!("๐Ÿ“ Agent create missing args response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + assert!(response.contains("error:"), "Missing error message part 1a"); + assert!(response.contains("the following required arguments"), "Missing error message part 1b"); + assert!(response.contains("were not provided:"), "Missing error message part 2"); + assert!(response.contains("--name"), "Missing required name argument part 1"); + assert!(response.contains(""), "Missing required name argument part 2"); + println!("โœ… Found error message for missing required arguments"); + + assert!(response.contains("Usage:"), "Missing usage information part 1"); + assert!(response.contains("/agent create"), "Missing usage information part 2a"); + assert!(response.contains("--name "), "Missing usage information part 2b"); + println!("โœ… Found usage information"); + + assert!(response.contains("For more information"), "Missing help suggestion part 1"); + assert!(response.contains("try"), "Missing help suggestion part 2a"); + println!("โœ… Found help suggestion"); + + assert!(response.contains("Options:"), "Missing options section"); + assert!(response.contains(""), "Missing name option part 2"); + assert!(response.contains("Name of the agent to be created"), "Missing name description"); + assert!(response.contains(""), "Missing directory option part 2"); + assert!(response.contains(""), "Missing from option part 2"); + println!("โœ… Found all expected options"); + + println!("โœ… /agent create executed successfully with expected error for missing arguments"); + + // Release the lock before cleanup + drop(chat); + + Ok(()) +} + +/// Tests the /agent help command to display comprehensive agent help information +/// Verifies agent descriptions, usage notes, launch instructions, and configuration paths +#[test] +#[cfg(all(feature = "agent", feature = "sanity"))] +fn test_agent_help_command() -> Result<(), Box> { + 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 = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("/agent help",Some(1000))?; + + println!("๐Ÿ“ Agent help command response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + assert!(response.contains("~/.aws/amazonq/cli-agents/"), "Missing global path"); + assert!(response.contains("cwd/.amazonq/cli-agents"), "Missing workspace path"); + assert!(response.contains("Usage:"), "Missing usage label"); + assert!(response.contains("/agent"), "Missing agent command"); + assert!(response.contains(""), "Missing command parameter"); + assert!(response.contains("Commands:"), "Missing commands section"); + assert!(response.contains("list"), "Missing list command"); + assert!(response.contains("create"), "Missing create command"); + assert!(response.contains("schema"), "Missing schema command"); + assert!(response.contains("set-default"), "Missing set-default command"); + assert!(response.contains("help"), "Missing help command"); + println!("โœ… Found all expected commands in help output"); + + assert!(response.contains("Options:"), "Missing options section"); + assert!(response.contains("-h"), "Missing short help flag"); + assert!(response.contains("--help"), "Missing long help flag"); + println!("โœ… Found all expected options in help output"); + + println!("โœ… All expected help content found"); + println!("โœ… /agent help executed successfully"); + + // Release the lock before cleanup + drop(chat); + + 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(all(feature = "agent", feature = "sanity"))] +fn test_agent_invalid_command() -> Result<(), Box> { + 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 =q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("/agent invalidcommand",Some(1000))?; + + println!("๐Ÿ“ Agent invalid command response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + assert!(response.contains("Commands:"), "Missing commands section"); + assert!(response.contains("list"), "Missing list command"); + assert!(response.contains("create"), "Missing create command"); + assert!(response.contains("schema"), "Missing schema command"); + assert!(response.contains("set-default"), "Missing set-default command"); + assert!(response.contains("help"), "Missing help command"); + println!("โœ… Found all expected commands in help output"); + + assert!(response.contains("Options:"), "Missing options section"); + println!("โœ… Found options section"); + + println!("โœ… /agent invalidcommand executed successfully with expected error"); + + // Release the lock before cleanup + drop(chat); + + Ok(()) +} + +/// Tests the /agent list command to display all available agents +/// Verifies agent listing format and presence of default agent +#[test] +#[cfg(all(feature = "agent", feature = "sanity"))] +fn test_agent_list_command() -> Result<(), Box> { + 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 = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("/agent list",Some(1000))?; + + println!("๐Ÿ“ Agent list response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + assert!(response.contains("q_cli_default"), "Missing q_cli_default agent"); + println!("โœ… Found q_cli_default agent in list"); + + assert!(response.contains("* q_cli_default"), "Missing bullet point format for q_cli_default"); + println!("โœ… Verified bullet point format for agent list"); + + println!("โœ… /agent list command executed successfully"); + + // Release the lock before cleanup + drop(chat); + + 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(all(feature = "agent", feature = "sanity"))] +fn test_agent_set_default_command() -> Result<(), Box> { + 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 =q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + let _ = chat.execute_command("clear"); + let _ = chat.execute_command("\x0C"); + + let response = chat.execute_command_with_timeout("/agent set-default -n q_cli_default",Some(1000))?; + + println!("๐Ÿ“ Agent set-default command response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + let mut failures = Vec::new(); + + if !response.contains("โœ“") { failures.push("Missing success checkmark"); } + if !response.contains("Default agent set to") { failures.push("Missing success message"); } + if !response.contains("q_cli_default") { failures.push("Missing agent name"); } + if !response.contains("This will take effect") { failures.push("Missing effect message"); } + if !response.contains("next time q chat is launched") { failures.push("Missing launch message"); } + + if !failures.is_empty() { + panic!("Test failures: {}", failures.join(", ")); + } + + println!("โœ… All expected success messages found"); + println!("โœ… /agent set-default executed successfully with valid arguments"); + + // Release the lock before cleanup + drop(chat); + + + Ok(()) +} +// Tests the /agent schema command to display agent configuration schema +// Verifies JSON schema structure with required keys and properties +#[test] +#[cfg(all(feature = "agent", feature = "sanity"))] +fn test_agent_schema_command() -> Result<(), Box> { + 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 = q_chat_helper::get_new_chat_session()?; + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + let schema_response = chat.execute_command_with_timeout("/agent schema",Some(1000))?; + + println!("๐Ÿ“ Agent schema response: {} bytes", schema_response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", schema_response); + println!("๐Ÿ“ END OUTPUT"); + + assert!(schema_response.contains("$schema"), "Missing $schema key"); + assert!(schema_response.contains("title"), "Missing title key"); + assert!(schema_response.contains("description"), "Missing description key"); + assert!(schema_response.contains("type"), "Missing type key"); + assert!(schema_response.contains("properties"), "Missing properties key"); + assert!(schema_response.contains("name"), "Missing name key"); + + println!("โœ… Found all expected JSON schema keys and properties"); + println!("โœ… /agent schema executed successfully with valid JSON schema"); + + drop(chat); + 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(all(feature = "agent", feature = "sanity"))] +fn test_agent_set_default_missing_args() -> Result<(), Box> { + 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 = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + let response = chat.execute_command_with_timeout("/agent set-default",Some(2000))?; + + println!("๐Ÿ“ Agent set-default missing args response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + let mut failures = Vec::new(); + + if !response.contains("error") { failures.push("Missing error message"); } + if !response.contains("the following required arguments were not provided:") { failures.push("Missing error message2"); } + if !response.contains("--name ") { failures.push("Missing required name argument"); } + if !response.contains("Usage:") { failures.push("Missing usage text"); } + if !response.contains("/agent") { failures.push("Missing agent command"); } + if !response.contains("set-default") { failures.push("Missing set-default subcommand"); } + if !response.contains("--name") { failures.push("Missing name flag"); } + if !response.contains("For more information") { failures.push("Missing help text"); } + if !response.contains("--help") { failures.push("Missing help flag"); } + if !response.contains("Options:") { failures.push("Missing options section"); } + if !response.contains("-n") { failures.push("Missing short name flag"); } + if !response.contains("") { failures.push("Missing name parameter"); } + if !response.contains("-h") { failures.push("Missing short help flag"); } + if !response.contains("Print help") { failures.push("Missing help description"); } + + if !failures.is_empty() { + panic!("Test failures: {}", failures.join(", ")); + } + + println!("โœ… All expected error messages and options found"); + println!("โœ… /agent set-default executed successfully with expected error for missing arguments"); + + // Release the lock before cleanup + drop(chat); + + Ok(()) +} + +/// Tests the /agent generate command to generate agent responses +/// Verifies agent generation process and response validation +#[test] +#[cfg(all(feature = "agent", feature = "sanity"))] +fn test_agent_generate_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing /agent generate command... | Description: Tests the /agent generatecommand with vi editor interaction"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + // Clear any previous session output to prevent contamination + let _ = chat.execute_command("clear"); + // Start the command and wait for name prompt + let _response1 = chat.execute_command_with_timeout("/agent generate", Some(20000))?; + // Wait longer for the prompt to fully appear + std::thread::sleep(std::time::Duration::from_secs(5)); + + // Enter agent name + chat.send_key_input("test-agent\r")?; + std::thread::sleep(std::time::Duration::from_secs(2)); + + // Enter description + chat.send_key_input("Test agent description\r")?; + std::thread::sleep(std::time::Duration::from_secs(2)); + + // Select scope (Enter for default) + chat.send_key_input("\r")?; + std::thread::sleep(std::time::Duration::from_secs(2)); + + // Wait for MCP menu, then confirm (Enter) + let _final_response = chat.send_key_input("\r")?; + std::thread::sleep(std::time::Duration::from_secs(2)); + + // Handle vi editor opening - enter insert mode and add content + chat.send_key_input("i")?; // Enter insert mode + // chat.send_key_input("Test system instructions for the agent")?; + chat.send_key_input("\u{1b}")?; // ESC to exit insert mode + + std::thread::sleep(std::time::Duration::from_secs(3)); + + // Get final response + let final_response = chat.execute_command(":wq")?; + println!("๐Ÿ“ Final response: {}", final_response); + + assert!( + final_response.contains("has been created and saved successfully") || + final_response.contains("Generating agent config") || + final_response.contains("Agent 'test-agent'"), + "Expected agent creation confirmation" + ); + drop(chat); + Ok(()) + +} + +// Tests the /agent swap command to swap the agents +// Verifies agent swap process and response validation +#[test] +#[cfg(all(feature = "agent", feature = "sanity"))] +fn test_agent_swap_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing /agent swap command... | Description: Tests the /agent swapcommand."); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + // Clear any previous session output to prevent contamination + let _ = chat.execute_command("clear"); + // Start the command and wait for name prompt + let _response1 = chat.execute_command_with_timeout("/agent swap",Some(2000))?; + println!("๐Ÿ“ Agent swap response: {} bytes", _response1.len()); + println!("๐Ÿ“ Full output: {}", _response1); + println!("๐Ÿ“ End output"); + let _response2 = chat.execute_command_with_timeout("1",Some(1000))?; + println!("๐Ÿ“ Agent swap response: {} bytes", _response2.len()); + println!("๐Ÿ“ Agent swap response Full output : {}", _response2); + + assert!( + _response2.contains("โœ“") || _response2.contains("Choose one of the following agents"), + "Expected agent swap confirmation" + ); + drop(chat); + Ok(()) +} \ No newline at end of file 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..41f922d9c2 --- /dev/null +++ b/e2etests/tests/ai_prompts/test_ai_prompt.rs @@ -0,0 +1,98 @@ +#[allow(unused_imports)] +use q_cli_e2e_tests::q_chat_helper; + +#[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 = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap(); + println!("โœ… Q Chat session started"); + + let response = chat.execute_command_with_timeout("What is AWS?",Some(1000))?; + + 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); + + 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 =q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap(); + println!("โœ… Q Chat session started"); + + let response = chat.execute_command_with_timeout("Hello",Some(1000))?; + + 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); + + Ok(()) +} diff --git a/e2etests/tests/ai_prompts/test_prompts_commands.rs b/e2etests/tests/ai_prompts/test_prompts_commands.rs new file mode 100644 index 0000000000..3ddc6b082f --- /dev/null +++ b/e2etests/tests/ai_prompts/test_prompts_commands.rs @@ -0,0 +1,160 @@ +#[allow(unused_imports)] +use q_cli_e2e_tests::q_chat_helper; + +#[test] +#[cfg(all(feature = "ai_prompts", feature = "sanity"))] +fn test_prompts_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing /prompts command... | Description: Tests the /prompts command to display available prompts with usage instructions and argument requirements"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap(); + + let response = chat.execute_command_with_timeout("/prompts",Some(1000))?; + + println!("๐Ÿ“ Prompts command response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify usage instruction + assert!(response.contains("Usage:") && response.contains("@") && response.contains("") && response.contains("[...args]"), "Missing usage instruction"); + println!("โœ… Found usage instruction"); + + // Verify table headers + assert!(response.contains("Prompt"), "Missing Prompt header"); + assert!(response.contains("Arguments") && response.contains("*") && response.contains("required"), "Missing Arguments header"); + println!("โœ… Found table headers with required notation"); + + // Verify command executed successfully + assert!(!response.is_empty(), "Empty response from prompts command"); + println!("โœ… Command executed with response"); + + println!("โœ… All prompts command functionality verified!"); + + // Release the lock before cleanup + drop(chat); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "ai_prompts", feature = "sanity"))] +fn test_prompts_help_command() -> Result<(), Box> { + 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 = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap(); + + let response = chat.execute_command_with_timeout("/prompts --help",Some(1000))?; + + println!("๐Ÿ“ Prompts help response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify description + assert!(response.contains("Prompts are reusable templates that help you quickly access common workflows and tasks"), "Missing prompts description"); + assert!(response.contains("These templates are provided by the mcp servers you have installed and configured"), "Missing MCP servers description"); + println!("โœ… Found prompts description"); + + // Verify usage examples + assert!(response.contains("@") && response.contains(" [arg]") && response.contains("[arg]"), "Missing @ syntax example"); + assert!(response.contains("Retrieve prompt specified"), "Missing retrieve description"); + assert!(response.contains("/prompts") && response.contains("get") && response.contains("") && response.contains("[arg]"), "Missing long form example"); + println!("โœ… Found usage examples with @ syntax and long form"); + + // Verify main description + assert!(response.contains("View and retrieve prompts"), "Missing main description"); + println!("โœ… Found main description"); + + // Verify Usage section + assert!(response.contains("Usage:") && response.contains("/prompts") && response.contains("[COMMAND]"), "Missing usage format"); + println!("โœ… Found usage format"); + + // Verify Commands section + assert!(response.contains("Commands:"), "Missing Commands section"); + assert!(response.contains("list"), "Missing list command"); + assert!(response.contains("get"), "Missing get command"); + assert!(response.contains("help"), "Missing help command"); + println!("โœ… Found all commands: list, get, help"); + + // Verify command descriptions + assert!(response.contains("List available prompts from a tool or show all available prompt"), "Missing list description"); + println!("โœ… Found command descriptions"); + + // Verify Options section + assert!(response.contains("Options:"), "Missing Options section"); + assert!(response.contains("-h") && response.contains("--help"), "Missing help flags"); + println!("โœ… Found Options section with help flags"); + + println!("โœ… All prompts help content verified!"); + + // Release the lock before cleanup + drop(chat); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "ai_prompts", feature = "sanity"))] +fn test_prompts_list_command() -> Result<(), Box> { + 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 = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap(); + + let response = chat.execute_command_with_timeout("/prompts list",Some(2000))?; + + println!("๐Ÿ“ Prompts list response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify usage instruction + assert!(response.contains("Usage:") && response.contains("@") && response.contains("") && response.contains("[...args]"), "Missing usage instruction"); + println!("โœ… Found usage instruction"); + + // Verify table headers + assert!(response.contains("Prompt"), "Missing Prompt header"); + assert!(response.contains("Arguments") && response.contains("*") && response.contains("required"), "Missing Arguments header"); + println!("โœ… Found table headers with required notation"); + + // Verify command executed successfully + assert!(!response.is_empty(), "Empty response from prompts list command"); + println!("โœ… Command executed with response"); + + println!("โœ… All prompts list command functionality verified!"); + + // Release the lock before cleanup + drop(chat); + + Ok(()) +} + + +#[test] +#[cfg(all(feature = "ai_prompts", feature = "sanity"))] +fn test_prompts_get_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing /prompts list command... | Description: Tests the /prompts get prompt_name command to display all available prompts with their arguments and usage information"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap(); + + let response = chat.execute_command_with_timeout("/prompts list",Some(2000))?; + println!("๐Ÿ“ Prompts list response: {}", response); + let first_prompt = response + .lines() + .find(|line| line.starts_with("- ")) // Find first line starting with "- " + .and_then(|line| line.strip_prefix("- ")) // Remove "- " prefix + .ok_or("No prompts found in list")?; + + assert!(!first_prompt.is_empty(), "No Prompts are available"); + println!("๐Ÿ“ First prompt found: {}", first_prompt); + + let get_response = chat.execute_command_with_timeout(&format!("/prompts get {}", first_prompt),Some(2000))?; + println!("๐Ÿ“ Get response: {}", get_response); + + assert!(get_response.is_empty() || !get_response.is_empty(), "Prompts contents can be or can not be empty."); + drop(chat); + Ok(()) +} \ No newline at end of file diff --git a/e2etests/tests/all_tests.rs b/e2etests/tests/all_tests.rs new file mode 100644 index 0000000000..79f5e748b1 --- /dev/null +++ b/e2etests/tests/all_tests.rs @@ -0,0 +1,21 @@ +// 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; +mod experiment; + +use q_cli_e2e_tests::q_chat_helper; + +#[ctor::dtor] +fn cleanup_session() { + let _ = q_chat_helper::close_session(); +} \ 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/context/test_context_command.rs b/e2etests/tests/context/test_context_command.rs new file mode 100644 index 0000000000..e84ec06fad --- /dev/null +++ b/e2etests/tests/context/test_context_command.rs @@ -0,0 +1,497 @@ +#[allow(unused_imports)] +use q_cli_e2e_tests::q_chat_helper; + +#[test] +#[cfg(all(feature = "context", feature = "sanity"))] +fn test_context_show_command() -> Result<(), Box> { + 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 = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("/context show",Some(500))?; + + println!("๐Ÿ“ Context show response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify context show output contains expected sections + 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"); + println!("โœ… Found all expected agent configuration files"); + + println!("โœ… All context show content verified!"); + + // Release the lock before cleanup + drop(chat); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "context", feature = "sanity"))] +fn test_context_help_command() -> Result<(), Box> { + 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 = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("/context help",Some(500))?; + + println!("๐Ÿ“ Context 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("/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("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"); + + println!("โœ… Found Options section with help flags"); + + println!("โœ… All context help content verified!"); + + // Release the lock before cleanup + drop(chat); + + + Ok(()) +} + +#[test] +#[cfg(all(feature = "context", feature = "sanity"))] +fn test_context_without_subcommand() -> Result<(), Box> { + 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 = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("/context",Some(500))?; + + println!("๐Ÿ“ Context response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + 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"); + + println!("โœ… All context help content verified!"); + + // Release the lock before cleanup + drop(chat); + + + Ok(()) +} + +#[test] +#[cfg(all(feature = "context", feature = "sanity"))] +fn test_context_invalid_command() -> Result<(), Box> { + 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 = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("/context test",Some(500))?; + + println!("๐Ÿ“ Context invalid response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify error message for invalid subcommand + assert!(response.contains("error"), "Missing error message"); + println!("โœ… Found expected error message for invalid subcommand"); + + println!("โœ… All context invalid command content verified!"); + + // Release the lock before cleanup + drop(chat); + + + Ok(()) +} + +#[test] +#[cfg(all(feature = "context", feature = "sanity"))] +fn test_add_non_existing_file_context() -> Result<(), Box> { + 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"; + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + // Try to add non-existing file to context + let add_response = chat.execute_command_with_timeout(&format!("/context add {}", non_existing_file_path),Some(1000))?; + + println!("๐Ÿ“ Context add response: {} bytes", add_response.len()); + println!("๐Ÿ“ ADD RESPONSE:"); + println!("{}", add_response); + println!("๐Ÿ“ END ADD RESPONSE"); + + // Verify error message for non-existing file + 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 + drop(chat); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "context", feature = "sanity"))] +fn test_context_remove_command_of_non_existent_file() -> Result<(), Box> { + 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 = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("/context remove non_existent_file.txt",Some(1000))?; + + println!("๐Ÿ“ Context remove response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify error message for non-existent file + assert!(response.contains("Error"), "Missing error message for non-existent file"); + println!("โœ… Found expected error message for non-existent file removal"); + + // Release the lock before cleanup + drop(chat); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "context", feature = "sanity"))] +fn test_add_remove_file_context() -> Result<(), Box> { + 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 + std::fs::write(test_file_path, "# Test file for context\nprint('Hello from test file')")?; + println!("โœ… Created test file at {}", test_file_path); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + // Add file to context + let add_response = chat.execute_command_with_timeout(&format!("/context add {}", test_file_path),Some(1000))?; + + println!("๐Ÿ“ Context add response: {} bytes", add_response.len()); + println!("๐Ÿ“ ADD RESPONSE:"); + println!("{}", add_response); + println!("๐Ÿ“ END ADD RESPONSE"); + + // Verify file was added successfully - be flexible with the exact message format + 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 + let show_response = chat.execute_command_with_timeout("/context show",Some(500))?; + + println!("๐Ÿ“ Context show response: {} bytes", show_response.len()); + println!("๐Ÿ“ SHOW RESPONSE:"); + println!("{}", show_response); + println!("๐Ÿ“ END SHOW RESPONSE"); + + // Verify file is present in context + assert!(show_response.contains(test_file_path), "File not found in context show output"); + println!("โœ… File confirmed present in context"); + + // Remove file from context + let remove_response = chat.execute_command_with_timeout(&format!("/context remove {}", test_file_path),Some(1000))?; + + println!("๐Ÿ“ Context remove response: {} bytes", remove_response.len()); + println!("๐Ÿ“ REMOVE RESPONSE:"); + println!("{}", remove_response); + println!("๐Ÿ“ END REMOVE RESPONSE"); + + // Verify file was removed successfully - be flexible with the exact message format + 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 + let final_show_response = chat.execute_command_with_timeout("/context show",Some(500))?; + + println!("๐Ÿ“ Final context show response: {} bytes", final_show_response.len()); + println!("๐Ÿ“ FINAL SHOW RESPONSE:"); + println!("{}", final_show_response); + println!("๐Ÿ“ END FINAL SHOW RESPONSE"); + + // Verify file is no longer in context + assert!(!final_show_response.contains(test_file_path), "File still found in context after removal"); + println!("โœ… File confirmed removed from context"); + + // Release the lock before cleanup + drop(chat); + + // Clean up test file + let _ = std::fs::remove_file(test_file_path); + println!("โœ… Cleaned up test file"); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "context", feature = "sanity"))] +fn test_add_glob_pattern_file_context()-> Result<(), Box> { + 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"; + let test_file3_path = "/tmp/test_context_file.js"; // Non-matching file + let glob_pattern = "/tmp/*.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); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + // Add glob pattern to context + let add_response = chat.execute_command_with_timeout(&format!("/context add {}", glob_pattern),Some(1000))?; + + println!("๐Ÿ“ Context add response: {} bytes", add_response.len()); + println!("๐Ÿ“ ADD RESPONSE:"); + println!("{}", add_response); + println!("๐Ÿ“ END ADD RESPONSE"); + + // Verify glob pattern was added successfully - be flexible with the exact message format + 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 + let show_response = chat.execute_command_with_timeout("/context show",Some(500))?; + + println!("๐Ÿ“ Context show response: {} bytes", show_response.len()); + println!("๐Ÿ“ SHOW RESPONSE:"); + println!("{}", show_response); + println!("๐Ÿ“ END SHOW RESPONSE"); + + // Verify glob pattern is present and matches files + assert!(show_response.contains(glob_pattern), "Glob pattern not found in context show output"); + println!("โœ… Glob pattern confirmed present in context with matches"); + + // Remove glob pattern from context + let remove_response = chat.execute_command_with_timeout(&format!("/context remove {}", glob_pattern),Some(1000))?; + + println!("๐Ÿ“ Context remove response: {} bytes", remove_response.len()); + println!("๐Ÿ“ REMOVE RESPONSE:"); + println!("{}", remove_response); + println!("๐Ÿ“ END REMOVE RESPONSE"); + + // Verify glob pattern was removed successfully - be flexible with the exact message format + 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 + let final_show_response = chat.execute_command_with_timeout("/context show",Some(1000))?; + + println!("๐Ÿ“ Final context show response: {} bytes", final_show_response.len()); + println!("๐Ÿ“ FINAL SHOW RESPONSE:"); + println!("{}", final_show_response); + println!("๐Ÿ“ END FINAL SHOW RESPONSE"); + + // Verify glob pattern is no longer in context + assert!(!final_show_response.contains(glob_pattern), "Glob pattern still found in context after removal"); + println!("โœ… Glob pattern confirmed removed from context"); + + // 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); + println!("โœ… Cleaned up test file"); + + Ok(()) +} + +#[test] +#[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_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 = q_chat_helper::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_with_timeout(&format!("/context add {} {} {}", test_file1_path, test_file2_path, test_file3_path),Some(1000))?; + + println!("๐Ÿ“ Context add response: {} bytes", add_response.len()); + println!("๐Ÿ“ ADD RESPONSE:"); + println!("{}", add_response); + println!("๐Ÿ“ END ADD RESPONSE"); + + // Verify files were added successfully - be flexible with the exact message format + 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_with_timeout("/context show",Some(500))?; + + println!("๐Ÿ“ Context show response: {} bytes", show_response.len()); + println!("๐Ÿ“ SHOW RESPONSE:"); + println!("{}", show_response); + println!("๐Ÿ“ END SHOW RESPONSE"); + + // 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_with_timeout(&format!("/context remove {} {} {}", test_file1_path, test_file2_path, test_file3_path),Some(1000))?; + + println!("๐Ÿ“ Context remove response: {} bytes", remove_response.len()); + println!("๐Ÿ“ REMOVE RESPONSE:"); + println!("{}", remove_response); + println!("๐Ÿ“ END REMOVE RESPONSE"); + + // 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 files are gone + let final_show_response = chat.execute_command_with_timeout("/context show",Some(500))?; + + println!("๐Ÿ“ Final context show response: {} bytes", final_show_response.len()); + println!("๐Ÿ“ FINAL SHOW RESPONSE:"); + println!("{}", final_show_response); + println!("๐Ÿ“ END FINAL SHOW RESPONSE"); + + // 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_file1_path); + let _ = std::fs::remove_file(test_file2_path); + let _ = std::fs::remove_file(test_file3_path); + println!("โœ… Cleaned up test file"); + + + Ok(()) +} + +#[test] +#[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_file_path, "# Test Python file 1 for context\nprint('Hello from Python file 1')")?; + println!("โœ… Created test files at {}", test_file_path); + + let session = q_chat_helper::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_with_timeout(&format!("/context add {}", test_file_path),Some(1000))?; + + println!("๐Ÿ“ Context add response: {} bytes", add_response.len()); + println!("๐Ÿ“ ADD RESPONSE:"); + println!("{}", add_response); + println!("๐Ÿ“ END ADD RESPONSE"); + + // Verify files were added successfully - be flexible with the exact message format + assert!(add_response.contains("Added"), "Missing success message for adding files"); + println!("โœ… Files added to context successfully"); + + // Execute /context show to confirm files are present + let show_response = chat.execute_command_with_timeout("/context show",Some(500))?; + + println!("๐Ÿ“ Context show response: {} bytes", show_response.len()); + println!("๐Ÿ“ SHOW RESPONSE:"); + 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_with_timeout("/context clear",Some(500))?; + + println!("๐Ÿ“ Context clear response: {} bytes", clear_response.len()); + println!("๐Ÿ“ CLEAR RESPONSE:"); + println!("{}", clear_response); + println!("๐Ÿ“ END CLEAR RESPONSE"); + + // Verify context was cleared successfully + assert!(clear_response.contains("Cleared context"), "Missing success message for clearing context"); + println!("โœ… Context cleared successfully"); + + // Execute /context show to confirm no files remain + let final_show_response = chat.execute_command_with_timeout("/context show",Some(500))?; + + println!("๐Ÿ“ Final context show response: {} bytes", final_show_response.len()); + println!("๐Ÿ“ FINAL SHOW RESPONSE:"); + 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(""), "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_file_path); + println!("โœ… Cleaned up test file"); + + + Ok(()) +} diff --git a/e2etests/tests/core_session/mod.rs b/e2etests/tests/core_session/mod.rs new file mode 100644 index 0000000000..5ba2476d38 --- /dev/null +++ b/e2etests/tests/core_session/mod.rs @@ -0,0 +1,6 @@ +pub mod test_clear_command; +pub mod test_help_command; +pub mod test_command_tangent; +pub mod test_quit_command; +pub mod test_changelog_command; +pub mod test_command_introspect; \ No newline at end of file diff --git a/e2etests/tests/core_session/test_changelog_command.rs b/e2etests/tests/core_session/test_changelog_command.rs new file mode 100644 index 0000000000..09e0666681 --- /dev/null +++ b/e2etests/tests/core_session/test_changelog_command.rs @@ -0,0 +1,79 @@ +#[allow(unused_imports)] +use q_cli_e2e_tests::q_chat_helper; + +#[allow(unused_imports)] +use regex::Regex; + +#[test] +#[cfg(all(feature = "changelog", feature = "sanity"))] +fn test_changelog_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing /changelog command... | Description: Tests the /changelog command to display version history and updates"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + println!("โœ… Q Chat session started"); + + let response = chat.execute_command_with_timeout("/changelog",Some(1000))?; + + println!("๐Ÿ“ Changelog response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify changelog content + assert!(response.contains("New") && response.contains("Amazon Q CLI"), "Missing changelog header"); + println!("โœ… Found changelog header"); + + // Verify version format (e.g., 1.16.2) + let version_regex = Regex::new(r"## \d+\.\d+\.\d+").unwrap(); + assert!(version_regex.is_match(&response), "Missing version format (x.x.x)"); + println!("โœ… Found valid version format"); + + // Verify date format (e.g., 2025-09-19) + let date_regex = Regex::new(r"\(\d{4}-\d{2}-\d{2}\)").unwrap(); + assert!(date_regex.is_match(&response), "Missing date format (YYYY-MM-DD)"); + println!("โœ… Found valid date format"); + + // Verify /changelog command reference + assert!(response.contains("/changelog"), "Missing /changelog command reference"); + println!("โœ… Found /changelog command reference"); + + println!("โœ… /changelog command test completed successfully"); + + // Release the lock + drop(chat); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "changelog", feature = "sanity"))] +fn test_changelog_help_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing /changelog -h command... | Description: Tests the /changelog -h command to display help information"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + println!("โœ… Q Chat session started"); + + let response = chat.execute_command_with_timeout("/changelog -h",Some(1000))?; + + println!("๐Ÿ“ Help response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify help content + assert!(response.contains("Usage:") && response.contains("/changelog"), "Missing usage information"); + assert!(response.contains("Options:"), "Missing Options section"); + assert!(response.contains("-h") && response.contains("--help"), "Missing -h, --help flags"); + println!("โœ… Found all expected help content"); + + println!("โœ… /changelog -h command test completed successfully"); + + // Release the lock + drop(chat); + + Ok(()) +} \ No newline at end of file diff --git a/e2etests/tests/core_session/test_clear_command.rs b/e2etests/tests/core_session/test_clear_command.rs new file mode 100644 index 0000000000..2cae1530ad --- /dev/null +++ b/e2etests/tests/core_session/test_clear_command.rs @@ -0,0 +1,44 @@ +#[allow(unused_imports)] +use q_cli_e2e_tests::q_chat_helper; + +#[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 = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + println!("โœ… Q Chat session started"); + + // Send initial message + println!("\n๐Ÿ” Sending prompt: 'My name is TestUser'"); + let _initial_response = chat.execute_command_with_timeout("My name is TestUser",Some(1000))?; + 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_with_timeout("/clear",Some(1000))?; + + println!("โœ… Clear command executed"); + + // Check if AI remembers previous conversation + println!("\n๐Ÿ” Sending prompt: 'What is my name?'"); + let test_response = chat.execute_command_with_timeout("What is my name?",Some(1000))?; + 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 + drop(chat); + + Ok(()) +} \ No newline at end of file diff --git a/e2etests/tests/core_session/test_command_introspect.rs b/e2etests/tests/core_session/test_command_introspect.rs new file mode 100644 index 0000000000..34c3c85e6d --- /dev/null +++ b/e2etests/tests/core_session/test_command_introspect.rs @@ -0,0 +1,33 @@ +#[allow(unused_imports)] +use q_cli_e2e_tests::q_chat_helper; + +//Test the introspect command +#[test] +#[cfg(all(feature = "core_session", feature = "sanity"))] +fn test_introspect_command() -> Result<(), Box> { + + println!("\n๐Ÿ” Testing introspect command... | Description: Tests the introspect command."); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + println!("โœ… Q Chat session started"); + + let response = chat.execute_command("introspect")?; + println!("๐Ÿ“ Help response: {} bytes", response); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Basic validation - check for key elements + assert!(!response.is_empty(), "Expected non-empty response"); + assert!(response.contains("Amazon Q"), "Missing Amazon Q identification"); + assert!(response.contains("assistant") || response.contains("AI"), "Missing AI assistant reference"); + assert!(response.contains("/quit") || response.contains("quit"), "Missing quit command"); + + println!("โœ… Introspect command executed successfully"); + + // Release the lock + drop(chat); + + Ok(()) +} \ No newline at end of file diff --git a/e2etests/tests/core_session/test_command_tangent.rs b/e2etests/tests/core_session/test_command_tangent.rs new file mode 100644 index 0000000000..d0cb561aca --- /dev/null +++ b/e2etests/tests/core_session/test_command_tangent.rs @@ -0,0 +1,28 @@ +#[allow(unused_imports)] +use q_cli_e2e_tests::q_chat_helper; + +// Test the tangent command. +#[test] +#[cfg(all(feature = "core_session", feature = "sanity"))] +fn test_tangent_command() -> Result<(), Box> { + +println!("\n๐Ÿ” Testing tangent ... | Description: Tests the /tangent command."); + let session =q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command("/tangent")?; + +println!("๐Ÿ“ transform response: {} bytes", response.len()); +println!("๐Ÿ“ FULL OUTPUT:"); +println!("{}", response); +println!("๐Ÿ“ END OUTPUT"); + +assert!(!response.is_empty(), "Expected non-empty response"); +assert!(response.contains("Created a conversation checkpoint") || response.contains("Restored conversation from checkpoint (โ†ฏ)") +|| response.contains("Tangent mode is disabled. Enable it with: q settings chat.enableTangentMode true"), "Expected checkpoint message"); + + drop(chat); + + + 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..fb0df6c801 --- /dev/null +++ b/e2etests/tests/core_session/test_help_command.rs @@ -0,0 +1,173 @@ +#[allow(unused_imports)] +use q_cli_e2e_tests::q_chat_helper; + +#[allow(dead_code)] +fn clean_terminal_output(input: &str) -> String { + input.replace("(B", "") +} + +#[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 = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + println!("โœ… Q Chat session started"); + + let response = chat.execute_command_with_timeout("/help",Some(100))?; + + 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 + drop(chat); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "help", feature = "sanity"))] +fn test_multiline_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing multiline input... | Description: Tests ctrl+J multiline command input with embedded newlines"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + println!("โœ… Q Chat session started"); + + // Ctrl+J produces ASCII Line Feed (0x0A) + let ctrl_j = "\x0A"; + let multiline_input = format!("what is aws explain in 100 words.{}what is AI explain in 100 words", ctrl_j); + let response = chat.execute_command_with_timeout(&multiline_input,Some(1000))?; + + println!("๐Ÿ“ Response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + assert!(response.contains("AWS"), "Response should contain 'AWS'"); + assert!(response.contains("AI"), "Response should contain 'AI'"); + assert!(!response.is_empty(), "Response should not be empty"); + println!("โœ… Multiline input processed successfully"); + + drop(chat); + Ok(()) +} + +#[test] +#[cfg(all(feature = "help", feature = "sanity"))] +fn test_whoami_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing !whoami command... | Description: Tests the !whoami command to display the current user"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + println!("โœ… Q Chat session started"); + + let response = chat.execute_command_with_timeout("!whoami",Some(100))?; + + println!("๐Ÿ“ Help response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify whoami content + assert!(!response.is_empty(), "Empty response from whoami command"); + println!("โœ… Command executed with response"); + + // Verify response contains user information + assert!(response.len() > 0, "Response should contain user information"); + println!("โœ… Found user information in response"); + + println!("โœ… All whoami command functionality verified!"); + + // Release the lock + drop(chat); + Ok(()) +} + +#[test] +#[cfg(all(feature = "help", feature = "sanity"))] +fn test_ctrls_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing ctrl+s input... | Description: Tests ctrl+scommand"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + println!("โœ… Q Chat session started"); + + // Ctrl+J produces ASCII Line Feed (0x0A) + let ctrl_j = "\x13"; + let response = chat.execute_command_with_timeout(ctrl_j,Some(100))?; + let cleaned_response = clean_terminal_output(&response); + + println!("๐Ÿ“ Response: {} bytes", cleaned_response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", cleaned_response); + println!("๐Ÿ“ END OUTPUT"); + assert!(cleaned_response.contains("agent"),"Response should contain /agent"); + assert!(cleaned_response.contains("editor"),"Response should contain /editor"); + assert!(cleaned_response.contains("clear"),"Response should contain /clear"); + assert!(cleaned_response.contains("experiment"),"Response should contain /experiment"); + assert!(cleaned_response.contains("context"),"Response should contain /context"); + + //pressing esc button to close ctrl+s window + let _esc = chat.execute_command("\x1B")?; + + drop(chat); + Ok(()) +} + +#[test] +#[cfg(all(feature = "help", feature = "sanity"))] +fn test_multiline_with_alt_enter_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing Alt(โŒฅ) + Enter(โŽ) input... | Description: Tests Alt(โŒฅ) + Enter(โŽ) for multiline input"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + println!("โœ… Q Chat session started"); + let alt_enter = "\x1B\x0A"; + let aws_prompt = "what is AWS explain in 100 words "; + let ai_rompt = "what is AI explain in 100 words"; + + let combined = format!("{}{}{}", aws_prompt, alt_enter,ai_rompt); + let response = chat.execute_command_with_timeout(&combined,Some(1000))?; + println!("๐Ÿ“ Response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT: {}",response); + println!("๐Ÿ“ END"); + + assert!(response.contains("AWS"), "Response should contain 'AWS'"); + assert!(response.contains("AI"), "Response should contain 'AI'"); + assert!(!response.is_empty(), "Response should not be empty"); + println!("โœ… Alt+Enter multiline input processed successfully"); + + drop(chat); + Ok(()) +} \ No newline at end of file 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..6192d5c7ad --- /dev/null +++ b/e2etests/tests/core_session/test_quit_command.rs @@ -0,0 +1,20 @@ +#[allow(unused_imports)] +use q_cli_e2e_tests::q_chat_helper; + +#[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 = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap(); + + println!("โœ… Q Chat session started"); + + chat.execute_command_with_timeout("/quit",Some(1000))?; + + println!("โœ… /quit command executed successfully"); + println!("โœ… Test completed successfully"); + + Ok(()) +} diff --git a/e2etests/tests/experiment/mod.rs b/e2etests/tests/experiment/mod.rs new file mode 100644 index 0000000000..f79f6c16cd --- /dev/null +++ b/e2etests/tests/experiment/mod.rs @@ -0,0 +1 @@ +pub mod test_experiment_command; diff --git a/e2etests/tests/experiment/test_experiment_command.rs b/e2etests/tests/experiment/test_experiment_command.rs new file mode 100644 index 0000000000..603820d622 --- /dev/null +++ b/e2etests/tests/experiment/test_experiment_command.rs @@ -0,0 +1,496 @@ +#[allow(unused_imports)] +use q_cli_e2e_tests::q_chat_helper; + +#[test] +#[cfg(all(feature = "experiment", feature = "sanity"))] +fn test_knowledge_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing /experiment command... | Description: Tests the /experiment command to toggle Knowledge experimental features"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + println!("โœ… Q Chat session started"); + + let response = chat.execute_command_with_timeout("/experiment",Some(500))?; + + println!("๐Ÿ“ Experiment response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify experiment menu content + assert!(response.contains("Select"), "Missing selection prompt"); + assert!(response.contains("Knowledge"), "Missing Knowledge experiment"); + println!("โœ… Found experiment menu with Knowledge option"); + + // Find Knowledge and check if it's already selected + let lines: Vec<&str> = response.lines().collect(); + let mut knowledge_menu_position = 0; + let mut knowledge_state = false; + let mut found = false; + let mut knowledge_already_selected = false; + + // Check if Knowledge is already selected (has โฏ) + for line in lines.iter() { + if line.contains("Knowledge") && line.trim_start().starts_with("โฏ") { + knowledge_already_selected = true; + knowledge_state = line.contains("[ON]"); + found = true; + break; + } + } + + // If not selected, find its position + if !knowledge_already_selected { + let mut menu_position = 0; + for line in lines.iter() { + let trimmed = line.trim_start(); + if trimmed.starts_with("โฏ") || (trimmed.contains("[ON]") || trimmed.contains("[OFF]")) { + if line.contains("Knowledge") { + knowledge_menu_position = menu_position; + knowledge_state = line.contains("[ON]"); + found = true; + break; + } + menu_position += 1; + } + } + } + + assert!(found, "Knowledge option not found in menu"); + println!("๐Ÿ“ Knowledge already selected: {}, position: {}, state: {}", knowledge_already_selected, knowledge_menu_position, if knowledge_state { "ON" } else { "OFF" }); + + // Navigate to Knowledge option using arrow keys (only if not already selected) + if !knowledge_already_selected { + for _ in 0..knowledge_menu_position { + chat.send_key_input("\x1b[B")?; // Down arrow + } + } + + // Select the Knowledge option + let navigate_response = chat.send_key_input("\r")?; // Enter + + println!("๐Ÿ“ Navigate response: {} bytes", navigate_response.len()); + println!("๐Ÿ“ NAVIGATE RESPONSE:"); + println!("{}", navigate_response); + println!("๐Ÿ“ END NAVIGATE RESPONSE"); + + // Verify toggle response based on previous state + if knowledge_state { + assert!(navigate_response.contains("Knowledge experiment disabled"), "Expected Knowledge to be disabled"); + println!("โœ… Knowledge experiment disabled successfully"); + } else { + assert!(navigate_response.contains("Knowledge experiment enabled"), "Expected Knowledge to be enabled"); + println!("โœ… Knowledge experiment enabled successfully"); + } + + // Test reverting back to original state (run command again) + println!("๐Ÿ“ Testing revert to original state..."); + chat.execute_command_with_timeout("/experiment",Some(500))?; + + // Navigate to Knowledge option again (only if not already selected) + if !knowledge_already_selected { + for _ in 0..knowledge_menu_position { + chat.send_key_input("\x1b[B")?; // Down arrow + } + } + let revert_navigate_response = chat.send_key_input("\r")?; // Enter + + println!("๐Ÿ“ Revert response: {} bytes", revert_navigate_response.len()); + println!("๐Ÿ“ REVERT RESPONSE:"); + println!("{}", revert_navigate_response); + println!("๐Ÿ“ END REVERT RESPONSE"); + + // Verify it reverted to original state + if knowledge_state { + assert!(revert_navigate_response.contains("Knowledge experiment enabled"), "Expected Knowledge to be enabled (reverted)"); + println!("โœ… Knowledge experiment reverted to enabled successfully"); + } else { + assert!(revert_navigate_response.contains("Knowledge experiment disabled"), "Expected Knowledge to be disabled (reverted)"); + println!("โœ… Knowledge experiment reverted to disabled successfully"); + } + + println!("โœ… /experiment command test completed successfully"); + + drop(chat); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "experiment", feature = "sanity"))] +fn test_thinking_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing /experiment command... | Description: Tests the /experiment command to toggle thinking experimental features"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + println!("โœ… Q Chat session started"); + + let response = chat.execute_command_with_timeout("/experiment",Some(500))?; + + println!("๐Ÿ“ Experiment response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify experiment menu content + assert!(response.contains("Select"), "Missing selection prompt"); + assert!(response.contains("Thinking"), "Missing Thinking experiment"); + println!("โœ… Found experiment menu with Thinking option"); + + // Find Thinking and check if it's already selected + let lines: Vec<&str> = response.lines().collect(); + let mut thinking_menu_position = 0; + let mut thinking_state = false; + let mut found = false; + let mut thinking_already_selected = false; + + // Check if Thinking is already selected (has โฏ) + for line in lines.iter() { + if line.contains("Thinking") && line.trim_start().starts_with("โฏ") { + thinking_already_selected = true; + thinking_state = line.contains("[ON]"); + found = true; + break; + } + } + + // If not selected, find its position + if !thinking_already_selected { + let mut menu_position = 0; + for line in lines.iter() { + let trimmed = line.trim_start(); + if trimmed.starts_with("โฏ") || (trimmed.contains("[ON]") || trimmed.contains("[OFF]")) { + if line.contains("Thinking") { + thinking_menu_position = menu_position; + thinking_state = line.contains("[ON]"); + found = true; + break; + } + menu_position += 1; + } + } + } + + assert!(found, "Thinking option not found in menu"); + println!("๐Ÿ“ Thinking already selected: {}, position: {}, state: {}", thinking_already_selected, thinking_menu_position, if thinking_state { "ON" } else { "OFF" }); + + // Navigate to Thinking option using arrow keys (only if not already selected) + if !thinking_already_selected { + for _ in 0..thinking_menu_position { + chat.send_key_input("\x1b[B")?; // Down arrow + } + } + + // Select the Thinking option + let navigate_response = chat.send_key_input("\r")?; // Enter + + println!("๐Ÿ“ Navigate response: {} bytes", navigate_response.len()); + println!("๐Ÿ“ NAVIGATE RESPONSE:"); + println!("{}", navigate_response); + println!("๐Ÿ“ END NAVIGATE RESPONSE"); + + // Verify toggle response based on previous state + if thinking_state { + assert!(navigate_response.contains("Thinking experiment disabled"), "Expected Thinking to be disabled"); + println!("โœ… Thinking experiment disabled successfully"); + } else { + assert!(navigate_response.contains("Thinking experiment enabled"), "Expected Thinking to be enabled"); + println!("โœ… Thinking experiment enabled successfully"); + } + + // Test reverting back to original state (run command again) + println!("๐Ÿ“ Testing revert to original state..."); + chat.execute_command_with_timeout("/experiment",Some(500))?; + + // Navigate to Thinking option again (only if not already selected) + if !thinking_already_selected { + for _ in 0..thinking_menu_position { + chat.send_key_input("\x1b[B")?; // Down arrow + } + } + let revert_navigate_response = chat.send_key_input("\r")?; // Enter + + println!("๐Ÿ“ Revert response: {} bytes", revert_navigate_response.len()); + println!("๐Ÿ“ REVERT RESPONSE:"); + println!("{}", revert_navigate_response); + println!("๐Ÿ“ END REVERT RESPONSE"); + + // Verify it reverted to original state + if thinking_state { + assert!(revert_navigate_response.contains("Thinking experiment enabled"), "Expected Thinking to be enabled (reverted)"); + println!("โœ… Thinking experiment reverted to enabled successfully"); + } else { + assert!(revert_navigate_response.contains("Thinking experiment disabled"), "Expected Thinking to be disabled (reverted)"); + println!("โœ… Thinking experiment reverted to disabled successfully"); + } + + println!("โœ… /experiment command test completed successfully"); + + drop(chat); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "experiment", feature = "sanity"))] +fn test_experiment_help_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing /experiment --help command... | Description: Tests the /experiment --help command to display help information"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + println!("โœ… Q Chat session started"); + + let response = chat.execute_command_with_timeout("/experiment --help",Some(500))?; + + println!("๐Ÿ“ Help response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify help content + assert!(response.contains("Usage:") && response.contains("/experiment"), "Missing usage information"); + assert!(response.contains("Options:"), "Missing Options section"); + assert!(response.contains("-h") && response.contains("--help"), "Missing -h, --help flags"); + println!("โœ… Found all expected help content"); + + println!("โœ… /experiment --help command test completed successfully"); + + drop(chat); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "experiment", feature = "sanity"))] +fn test_tangent_mode_experiment() -> Result<(), Box> { + println!("\n๐Ÿ” Testing Tangent Mode experiment... | Description: Tests the /experiment command to toggle Tangent Mode experimental feature"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + println!("โœ… Q Chat session started"); + + let response = chat.execute_command_with_timeout("/experiment",Some(500))?; + + println!("๐Ÿ“ Experiment response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify experiment menu content + assert!(response.contains("Select"), "Missing selection prompt"); + assert!(response.contains("Tangent Mode"), "Missing Tangent Mode experiment"); + println!("โœ… Found experiment menu with Tangent Mode option"); + + // Find Tangent Mode and check if it's already selected + let lines: Vec<&str> = response.lines().collect(); + let mut tangent_menu_position = 0; + let mut tangent_state = false; + let mut found = false; + let mut tangent_already_selected = false; + + // Check if Tangent Mode is already selected (has โฏ) + for line in lines.iter() { + if line.contains("Tangent Mode") && line.trim_start().starts_with("โฏ") { + tangent_already_selected = true; + tangent_state = line.contains("[ON]"); + found = true; + break; + } + } + + // If not selected, find its position + if !tangent_already_selected { + let mut menu_position = 0; + for line in lines.iter() { + let trimmed = line.trim_start(); + if trimmed.starts_with("โฏ") || (trimmed.contains("[ON]") || trimmed.contains("[OFF]")) { + if line.contains("Tangent Mode") { + tangent_menu_position = menu_position; + tangent_state = line.contains("[ON]"); + found = true; + break; + } + menu_position += 1; + } + } + } + + assert!(found, "Tangent Mode option not found in menu"); + println!("๐Ÿ“ Tangent Mode already selected: {}, position: {}, state: {}", tangent_already_selected,tangent_menu_position, if tangent_state { "ON" } else { "OFF" }); + + // Navigate to Tangent Mode option using arrow keys (only if not already selected) + if !tangent_already_selected { + for _ in 0..tangent_menu_position { + chat.send_key_input("\x1b[B")?; // Down arrow + } + } + + // Select the Tangent Mode option + let navigate_response = chat.send_key_input("\r")?; // Enter + + println!("๐Ÿ“ Navigate response: {} bytes", navigate_response.len()); + println!("๐Ÿ“ NAVIGATE RESPONSE:"); + println!("{}", navigate_response); + println!("๐Ÿ“ END NAVIGATE RESPONSE"); + + // Verify toggle response based on previous state + if tangent_state { + assert!(navigate_response.contains("Tangent Mode experiment disabled"), "Expected Tangent Mode to be disabled"); + println!("โœ… Tangent Mode experiment disabled successfully"); + } else { + assert!(navigate_response.contains("Tangent Mode experiment enabled"), "Expected Tangent Mode to be enabled"); + println!("โœ… Tangent Mode experiment enabled successfully"); + } + + // Test reverting back to original state (run command again) + println!("๐Ÿ“ Testing revert to original state..."); + chat.execute_command_with_timeout("/experiment",Some(500))?; + + // Navigate to Tangent Mode option again (only if not already selected) + if !tangent_already_selected { + for _ in 0..tangent_menu_position { + chat.send_key_input("\x1b[B")?; // Down arrow + } + } + let revert_navigate_response = chat.send_key_input("\r")?; // Enter + + println!("๐Ÿ“ Revert response: {} bytes", revert_navigate_response.len()); + println!("๐Ÿ“ REVERT RESPONSE:"); + println!("{}", revert_navigate_response); + println!("๐Ÿ“ END REVERT RESPONSE"); + + // Verify it reverted to original state + if tangent_state { + assert!(revert_navigate_response.contains("Tangent Mode experiment enabled"), "Expected Tangent Mode to be enabled (reverted)"); + println!("โœ… Tangent Mode experiment reverted to enabled successfully"); + } else { + assert!(revert_navigate_response.contains("Tangent Mode experiment disabled"), "Expected Tangent Mode to be disabled (reverted)"); + println!("โœ… Tangent Mode experiment reverted to disabled successfully"); + } + + println!("โœ… Tangent Mode experiment test completed successfully"); + + drop(chat); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "experiment", feature = "sanity"))] +fn test_todo_lists_experiment() -> Result<(), Box> { + println!("\n๐Ÿ” Testing Todo Lists experiment... | Description: Tests the /experiment command to toggle Todo Lists experimental feature"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + println!("โœ… Q Chat session started"); + + let response = chat.execute_command_with_timeout("/experiment",Some(500))?; + + println!("๐Ÿ“ Experiment response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify experiment menu content + assert!(response.contains("Select"), "Missing selection prompt"); + assert!(response.contains("Todo Lists"), "Missing Todo Lists experiment"); + println!("โœ… Found experiment menu with Todo Lists option"); + + // Find Todo Lists and check if it's already selected + let lines: Vec<&str> = response.lines().collect(); + let mut todo_lists_menu_position = 0; + let mut todo_lists_state = false; + let mut found = false; + let mut todo_lists_already_selected = false; + + // Check if Todo Lists is already selected (has โฏ) + for line in lines.iter() { + if line.contains("Todo Lists") && line.trim_start().starts_with("โฏ") { + todo_lists_already_selected = true; + todo_lists_state = line.contains("[ON]"); + found = true; + break; + } + } + + // If not selected, find its position + if !todo_lists_already_selected { + let mut menu_position = 0; + for line in lines.iter() { + let trimmed = line.trim_start(); + if trimmed.starts_with("โฏ") || (trimmed.contains("[ON]") || trimmed.contains("[OFF]")) { + if line.contains("Todo Lists") { + todo_lists_menu_position = menu_position; + todo_lists_state = line.contains("[ON]"); + found = true; + break; + } + menu_position += 1; + } + } + } + + assert!(found, "Todo Lists option not found in menu"); + println!("๐Ÿ“ Todo Lists already selected: {}, position: {}, state: {}", todo_lists_already_selected, todo_lists_menu_position, if todo_lists_state { "ON" } else { "OFF" }); + + // Navigate to Todo Lists option using arrow keys (only if not already selected) + if !todo_lists_already_selected { + for _ in 0..todo_lists_menu_position { + chat.send_key_input("\x1b[B")?; // Down arrow + } + } + + // Select the Todo Lists option + let navigate_response = chat.send_key_input("\r")?; // Enter + + println!("๐Ÿ“ Navigate response: {} bytes", navigate_response.len()); + println!("๐Ÿ“ NAVIGATE RESPONSE:"); + println!("{}", navigate_response); + println!("๐Ÿ“ END NAVIGATE RESPONSE"); + + // Verify toggle response based on previous state + if todo_lists_state { + assert!(navigate_response.contains("Todo Lists experiment disabled"), "Expected Todo Lists to be disabled"); + println!("โœ… Todo Lists experiment disabled successfully"); + } else { + assert!(navigate_response.contains("Todo Lists experiment enabled"), "Expected Todo Lists to be enabled"); + println!("โœ… Todo Lists experiment enabled successfully"); + } + + // Test reverting back to original state (run command again) + println!("๐Ÿ“ Testing revert to original state..."); + chat.execute_command_with_timeout("/experiment",Some(500))?; + + // Navigate to Todo Lists option again (only if not already selected) + if !todo_lists_already_selected { + for _ in 0..todo_lists_menu_position { + chat.send_key_input("\x1b[B")?; // Down arrow + } + } + let revert_navigate_response = chat.send_key_input("\r")?; // Enter + + println!("๐Ÿ“ Revert response: {} bytes", revert_navigate_response.len()); + println!("๐Ÿ“ REVERT RESPONSE:"); + println!("{}", revert_navigate_response); + println!("๐Ÿ“ END REVERT RESPONSE"); + + // Verify it reverted to original state + if todo_lists_state { + assert!(revert_navigate_response.contains("Todo Lists experiment enabled"), "Expected Todo Lists to be enabled (reverted)"); + println!("โœ… Todo Lists experiment reverted to enabled successfully"); + } else { + assert!(revert_navigate_response.contains("Todo Lists experiment disabled"), "Expected Todo Lists to be disabled (reverted)"); + println!("โœ… Todo Lists experiment reverted to disabled successfully"); + } + + println!("โœ… Todo Lists experiment test completed successfully"); + + drop(chat); + + Ok(()) +} \ No newline at end of file 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..5afa326934 --- /dev/null +++ b/e2etests/tests/integration/test_editor_help_command.rs @@ -0,0 +1,290 @@ +#[allow(unused_imports)] +use q_cli_e2e_tests::q_chat_helper; + +#[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 = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("/editor --help",Some(500))?; + + 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 + drop(chat); + + 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 = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("/help editor",Some(500))?; + + 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 + drop(chat); + + 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 = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("/editor -h",Some(500))?; + + 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 + drop(chat); + + 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 = q_chat_helper::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_with_timeout("/editor",Some(500))?; + + 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.send_key_input("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.send_key_input("\x1b")?; // ESC key + println!("๐Ÿ“ Esc response: {} bytes", esc_response.len()); + + // Execute :wq to save and quit + let wq_response = chat.send_key_input(":wq\r")?; + + 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 + drop(chat); + + 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 = q_chat_helper::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_with_timeout("/editor nonexistent_file.txt",Some(500))?; + + 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.send_key_input("i")?; + println!("๐Ÿ“ Insert mode response: {} bytes", insert_response.len()); + + + // Press Esc to exit insert mode + let esc_response = chat.send_key_input("\x1b")?; // ESC key + println!("๐Ÿ“ Esc response: {} bytes", esc_response.len()); + + // Execute :wq to save and quit + let wq_response = chat.send_key_input(":wq\r")?; + + 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 + drop(chat); + + 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 = q_chat_helper::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_with_timeout(&format!("/editor {}", test_file_path),Some(500))?; + + 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.send_key_input("i")?; + println!("๐Ÿ“ Insert mode response: {} bytes", insert_response.len()); + + + // Press Esc to exit insert mode + let esc_response = chat.send_key_input("\x1b")?; // ESC key + println!("๐Ÿ“ Esc response: {} bytes", esc_response.len()); + + // Execute :wq to save and quit + let wq_response = chat.send_key_input(":wq\r")?; + + println!("๐Ÿ“ Final wq response: {} bytes", wq_response.len()); + println!("๐Ÿ“ WQ RESPONSE:"); + println!("{}", wq_response); + println!("๐Ÿ“ END WQ RESPONSE"); + + + if wq_response.contains("Using tool:") && wq_response.contains("Allow this action?"){ + 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 the file content is loaded in editor + assert!(allow_response.contains("Hello from test file"), "File content not loaded in editor"); + println!("โœ… File content loaded successfully in editor"); + + } + else{ + // 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 + drop(chat); + + 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..ef55e8f1bc --- /dev/null +++ b/e2etests/tests/integration/test_hooks_command.rs @@ -0,0 +1,98 @@ +#[allow(unused_imports)] +use q_cli_e2e_tests::q_chat_helper; + +#[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 = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("/hooks",Some(500))?; + + 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!"); + + drop(chat); + + 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 = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("/hooks --help",Some(500))?; + + 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!"); + + drop(chat); + + 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 = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("/hooks -h",Some(500))?; + + 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!"); + + drop(chat); + + 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..999cc92d8d --- /dev/null +++ b/e2etests/tests/integration/test_issue_command.rs @@ -0,0 +1,153 @@ +#[allow(unused_imports)] +use q_cli_e2e_tests::q_chat_helper; + +#[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 = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("/issue \"Bug: Q CLI crashes when using large files\"",Some(3000))?; + + 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!"); + + drop(chat); + + 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 = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("/issue --force Critical bug in file handling",Some(3000))?; + + println!("๐Ÿ“ Issue force command response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify command executed successfully (GitHub opens automatically or shows command) + assert!(response.contains("Heading over to GitHub...") || response.contains("/issue --force") || !response.trim().is_empty(), "Command should execute or show in history"); + println!("โœ… Command executed successfully"); + + println!("โœ… All issue --force command functionality verified!"); + + drop(chat); + + 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 = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("/issue -f \"Critical bug in file handling\"",Some(3000))?; + + 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!"); + + drop(chat); + + 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 = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("/issue --help",Some(3000))?; + + 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!"); + + drop(chat); + + 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 = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("/issue -h",Some(3000))?; + + 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!"); + + drop(chat); + + 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..1e643242c4 --- /dev/null +++ b/e2etests/tests/integration/test_subscribe_command.rs @@ -0,0 +1,143 @@ +#[allow(unused_imports)] +use q_cli_e2e_tests::q_chat_helper; + + +#[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 = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("/subscribe",Some(500))?; + + 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!"); + + drop(chat); + + 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 = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("/subscribe --manage",Some(500))?; + + 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!"); + + drop(chat); + + 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 = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("/subscribe --help",Some(500))?; + + 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!"); + + drop(chat); + + 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 = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("/subscribe -h",Some(500))?; + + 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!"); + + drop(chat); + + 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..d7b3c57f34 --- /dev/null +++ b/e2etests/tests/mcp/test_mcp_command.rs @@ -0,0 +1,78 @@ +#[allow(unused_imports)] +use q_cli_e2e_tests::q_chat_helper; + +#[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 = q_chat_helper::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!"); + + drop(chat); + + 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 = q_chat_helper::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!"); + + drop(chat); + + Ok(()) +} + diff --git a/e2etests/tests/mcp/test_mcp_command_regression.rs b/e2etests/tests/mcp/test_mcp_command_regression.rs new file mode 100644 index 0000000000..ffc352efaa --- /dev/null +++ b/e2etests/tests/mcp/test_mcp_command_regression.rs @@ -0,0 +1,562 @@ +#[allow(unused_imports)] +use q_cli_e2e_tests::q_chat_helper; + +#[test] +#[cfg(all(feature = "mcp", feature = "regression"))] +fn test_mcp_remove_help_command() -> Result<(), Box> { + 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 = q_chat_helper::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_with_timeout("execute below bash command q mcp remove --help",Some(1000))?; + + println!("๐Ÿ“ MCP remove help response: {} bytes", help_response.len()); + println!("๐Ÿ“ HELP RESPONSE:"); + println!("{}", help_response); + println!("๐Ÿ“ END HELP RESPONSE"); + + // Verify tool execution prompt appears + 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 + let allow_response = chat.execute_command_with_timeout("y",Some(500))?; + + println!("๐Ÿ“ Allow response: {} bytes", allow_response.len()); + println!("๐Ÿ“ ALLOW RESPONSE:"); + println!("{}", allow_response); + println!("๐Ÿ“ END ALLOW RESPONSE"); + + // Verify complete help content in final response + 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"); + println!("โœ… Found all expected MCP remove help content and completion"); + + drop(chat); + + Ok(()) +} + +#[test] +#[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 = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + // Execute mcp add --help command + println!("\n๐Ÿ” Executing command: 'q mcp add --help'"); + let response = chat.execute_command_with_timeout("execute below bash command q mcp add --help",Some(1000))?; + + println!("๐Ÿ“ Restart response: {} bytes", response.len()); + println!("๐Ÿ“ RESTART RESPONSE:"); + println!("{}", response); + println!("๐Ÿ“ END RESTART RESPONSE"); + + // Verify tool execution details + assert!(response.contains("q mcp add --help"), "Missing command execution description"); + assert!(response.contains("Purpose"), "Missing purpose description"); + println!("โœ… Found tool execution details"); + + // Verify tool execution prompt appears + 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_with_timeout("y",Some(500))?; + + println!("๐Ÿ“ Allow response: {} bytes", allow_response.len()); + println!("๐Ÿ“ ALLOW RESPONSE:"); + println!("{}", allow_response); + println!("๐Ÿ“ END ALLOW RESPONSE"); + + // Verify mcp add help output + 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("--agent "), "Missing --agent option"); + assert!(allow_response.contains("--force"), "Missing --force 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"); + assert!(allow_response.contains("Optional"), "Missing Optional indicator"); + println!("โœ… MCP add help command executed successfully"); + + drop(chat); + + Ok(()) +} + +#[test] +#[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 = q_chat_helper::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_with_timeout("execute below bash command q mcp --help",Some(1000))?; + + println!("๐Ÿ“ MCP help response: {} bytes", help_response.len()); + println!("๐Ÿ“ HELP RESPONSE:"); + println!("{}", help_response); + println!("๐Ÿ“ END HELP RESPONSE"); + + // Verify tool execution prompt appears + 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 + let allow_response = chat.execute_command_with_timeout("y",Some(500))?; + + println!("๐Ÿ“ Allow response: {} bytes", allow_response.len()); + println!("๐Ÿ“ ALLOW RESPONSE:"); + println!("{}", allow_response); + println!("๐Ÿ“ END ALLOW RESPONSE"); + + // Verify complete help content + assert!(allow_response.contains("Model Context Protocol (MCP)"), "Missing MCP description"); + 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"); + assert!(allow_response.contains("remove"), "Missing remove command description"); + assert!(allow_response.contains("list"), "Missing list command description"); + assert!(allow_response.contains("import"), "Missing import command description"); + assert!(allow_response.contains("status"), "Missing status command description"); + assert!(allow_response.contains("help"), "Missing help command"); + println!("โœ… Found all MCP commands with descriptions"); + + 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"); + println!("โœ… Found all expected MCP help content and completion"); + + drop(chat); + + Ok(()) +} + +#[test] +#[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 = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + // Execute mcp import --help command + println!("\n๐Ÿ” Executing command: 'q mcp import --help'"); + let response = chat.execute_command_with_timeout("execute below bash command q mcp import --help",Some(1000))?; + + println!("๐Ÿ“ Restart response: {} bytes", response.len()); + println!("๐Ÿ“ RESTART RESPONSE:"); + println!("{}", response); + println!("๐Ÿ“ END RESTART RESPONSE"); + + // Verify tool execution details + assert!(response.contains("q mcp import --help"), "Missing command execution description"); + assert!(response.contains("Purpose"), "Missing purpose description"); + println!("โœ… Found tool execution details"); + + // Verify tool execution prompt appears + 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_with_timeout("y",Some(500))?; + + println!("๐Ÿ“ Allow response: {} bytes", allow_response.len()); + println!("๐Ÿ“ ALLOW RESPONSE:"); + println!("{}", allow_response); + println!("๐Ÿ“ END ALLOW RESPONSE"); + + // Verify usage line + assert!(allow_response.contains("Usage"), "Missing complete usage line"); + println!("โœ… Found usage information"); + + // Verify Arguments section + assert!(allow_response.contains("Arguments"), "Missing Arguments section"); + println!("โœ… Found Arguments section with SCOPE"); + + // Verify Options section + assert!(allow_response.contains("Options"), "Missing Options section"); + assert!(allow_response.contains("--file "), "Missing --file option"); + assert!(allow_response.contains("--force"), "Missing --force 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"); + + println!("โœ… All q mcp import --help content verified successfully"); + + drop(chat); + + Ok(()) +} + +#[test] +#[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 = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("execute below bash command q mcp list",Some(1000))?; + + println!("๐Ÿ“ MCP list response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify tool execution prompt + 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?"), "Missing permission prompt"); + println!("โœ… Found tool execution permission prompt"); + + // Allow the tool execution + let allow_response = chat.execute_command_with_timeout("y",Some(500))?; + + println!("๐Ÿ“ Allow response: {} bytes", allow_response.len()); + println!("๐Ÿ“ ALLOW RESPONSE:"); + println!("{}", allow_response); + println!("๐Ÿ“ END ALLOW RESPONSE"); + + + // Verify MCP server listing + assert!(allow_response.contains("q_cli_default"), "Missing q_cli_default server"); + println!("โœ… Found MCP server listing with servers and completion"); + + drop(chat); + + Ok(()) +} + +#[test] +#[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 = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("execute below bash command q mcp list --help",Some(1000))?; + + println!("๐Ÿ“ MCP list help response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify tool execution prompt + 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?"), "Missing permission prompt"); + println!("โœ… Found tool execution permission prompt"); + + // Allow the tool execution + let allow_response = chat.execute_command_with_timeout("y",Some(500))?; + + println!("๐Ÿ“ Allow response: {} bytes", allow_response.len()); + println!("๐Ÿ“ ALLOW RESPONSE:"); + println!("{}", allow_response); + println!("๐Ÿ“ END ALLOW RESPONSE"); + + // Verify help content + assert!(allow_response.contains("Usage"), "Missing usage format"); + + // Verify arguments section + assert!(allow_response.contains("Arguments"), "Missing Arguments section"); + assert!(allow_response.contains("[SCOPE]"), "Missing scope argument"); + + // Verify options section + 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"); + + drop(chat); + + Ok(()) +} + +#[test] +#[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 = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + // Execute mcp status --help command + println!("\n๐Ÿ” Executing command: 'q mcp status --help'"); + let response = chat.execute_command_with_timeout("execute below bash command q mcp status --help",Some(1000))?; + + println!("๐Ÿ“ Restart response: {} bytes", response.len()); + println!("๐Ÿ“ RESTART RESPONSE:"); + println!("{}", response); + println!("๐Ÿ“ END RESTART RESPONSE"); + + // Verify tool execution details + assert!(response.contains("Purpose"), "Missing purpose description"); + println!("โœ… Found tool execution details"); + + // Verify tool execution prompt appears + 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_with_timeout("y",Some(500))?; + + println!("๐Ÿ“ Allow response: {} bytes", allow_response.len()); + println!("๐Ÿ“ ALLOW RESPONSE:"); + println!("{}", allow_response); + println!("๐Ÿ“ END ALLOW RESPONSE"); + + // Verify 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("-h, --help"), "Missing --help option"); + println!("โœ… Found all options with descriptions"); + + println!("โœ… All q mcp status --help content verified successfully"); + + drop(chat); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "mcp", feature = "regression"))] +fn test_add_and_remove_mcp_command() -> Result<(), Box> { + 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!("\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 = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + // 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_with_timeout("execute below bash command q mcp list",Some(1000))?; + + 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_with_timeout("y",Some(500))?; + 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_with_timeout("execute below bash command q mcp remove --name aws-documentation",Some(1000))?; + 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_with_timeout("y",Some(500))?; + 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_with_timeout("execute below bash command q mcp add --name aws-documentation --command uvx --args awslabs.aws-documentation-mcp-server@latest",Some(2000))?; + + 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"); + 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_with_timeout("y",Some(500))?; + 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"); + assert!(allow_response.contains("/Users/") && allow_response.contains("/.aws/amazonq/mcp.json"), "Missing config file path"); + println!("โœ… Found successful addition message"); + + // Now test removing the MCP server + println!("\n๐Ÿ” Executing remove command: 'q mcp remove --name aws-documentation'"); + let remove_response = chat.execute_command_with_timeout("execute below bash command q mcp remove --name aws-documentation",Some(2000))?; + 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_with_timeout("y",Some(500))?; + 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"); + + drop(chat); + 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 = q_chat_helper::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_with_timeout("execute below bash command q mcp add --name aws-documentation --command uvx --args awslabs.aws-documentation-mcp-server@latest",Some(2000))?; + + 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_with_timeout("y",Some(500))?; + 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_with_timeout("execute below bash command q mcp status --name aws-documentation",Some(2000))?; + 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_with_timeout("y",Some(500))?; + 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_with_timeout("execute below bash command q mcp remove --name aws-documentation",Some(2000))?; + 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_with_timeout("y",Some(500))?; + 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"); + + drop(chat); + Ok(()) +} 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/model/test_model_dynamic_command.rs b/e2etests/tests/model/test_model_dynamic_command.rs new file mode 100644 index 0000000000..ca94e3e4e2 --- /dev/null +++ b/e2etests/tests/model/test_model_dynamic_command.rs @@ -0,0 +1,187 @@ +#[allow(unused_imports)] +use q_cli_e2e_tests::q_chat_helper; + +#[test] +#[cfg(all(feature = "model", feature = "sanity"))] +fn test_model_dynamic_command() -> Result<(), Box> { + 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 = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + // Execute /model command to get list + let model_response = chat.execute_command_with_timeout("/model",Some(1000))?; + + println!("๐Ÿ“ Model response: {} bytes", model_response.len()); + println!("๐Ÿ“ MODEL RESPONSE:"); + println!("{}", model_response); + println!("๐Ÿ“ END MODEL RESPONSE"); + + // Helper function to strip ANSI color codes + let strip_ansi = |s: &str| -> String { + let mut result = String::new(); + let mut in_escape = false; + for c in s.chars() { + if c == '\x1b' { + in_escape = true; + } else if in_escape && c == 'm' { + in_escape = false; + } else if !in_escape { + result.push(c); + } + } + result + }; + + // Parse available models from response + let mut models = Vec::new(); + let mut found_prompt = false; + + for line in model_response.lines() { + let trimmed_line = line.trim(); + + // Look for the prompt line + if trimmed_line.contains("Select a model for this chat session") { + found_prompt = true; + continue; + } + + // After finding prompt, parse model lines + if found_prompt { + let cleaned_line = strip_ansi(trimmed_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) + if cleaned_line.starts_with("โฏ") || cleaned_line.starts_with(" ") || cleaned_line.contains("-") { + let model_name = cleaned_line + .replace("โฏ", "") + .replace("(active)", "") + .trim() + .to_string(); + + println!("\n๐Ÿ” Extracted model: '{}'", model_name); + if !model_name.is_empty() { + models.push(model_name); + } + } + } + } + } + + println!("๐Ÿ“ Found models: {:?}", models); + assert!(!models.is_empty(), "No models found in response"); + + // Send down arrow to select different model + let selection_response = chat.send_key_input("\x1b[B")?; + + println!("๐Ÿ“ Selection response: {} bytes", selection_response.len()); + println!("๐Ÿ“ SELECTION RESPONSE:"); + println!("{}", selection_response); + println!("๐Ÿ“ END SELECTION RESPONSE"); + + // Find which model is now selected (has โฏ marker) + let selected_model = selection_response.lines() + .find(|line| { + let cleaned = strip_ansi(line); + cleaned.contains("โฏ") + }) + .map(|line| { + let cleaned = strip_ansi(line.trim()); + cleaned + .replace("โฏ", "") + .replace("(active)", "") + .trim() + .to_string() + }) + .unwrap_or_else(|| models.get(1).unwrap_or(&models[0]).clone()); + + println!("๐Ÿ“ Selected model: {}", selected_model); + + // Send Enter to confirm + let confirm_response = chat.send_key_input("\r")?; + + println!("๐Ÿ“ Confirm response: {} bytes", confirm_response.len()); + println!("๐Ÿ“ CONFIRM RESPONSE:"); + println!("{}", confirm_response); + println!("๐Ÿ“ END CONFIRM RESPONSE"); + + // Verify selection with dynamic model name + assert!(confirm_response.contains(&format!("Using {}", selected_model)), + "Missing confirmation for selected model: {}", selected_model); + println!("โœ… Confirmed selection of: {}", selected_model); + + drop(chat); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "model", feature = "sanity"))] +fn test_model_help_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing /model --help command... | Description: Tests the /model --help command to display help information for model selection functionality"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("/model --help",Some(500))?; + + 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!"); + + drop(chat); + + 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 = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("/model -h",Some(500))?; + + 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!"); + + drop(chat); + + 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..38d48c0522 --- /dev/null +++ b/e2etests/tests/q_subcommand/mod.rs @@ -0,0 +1,13 @@ +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; +pub mod test_q_update_subcommand; +pub mod test_q_restart_subcommand; +pub mod test_q_user_subcommand; +pub mod test_q_settings_format_command; +pub mod test_q_settings_deletecommand; +pub mod test_q_quit_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 q debug build dashboard subcommand to get current dashboard build version."); + + println!("\n๐Ÿ” Executing 'q debug build dashboard' subcommand..."); + let response = q_chat_helper::execute_q_subcommand("q", &["debug", "build", "dashboard"])?; + + 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 dashboard output ({} bytes)!", response.len()); + println!("โœ… q debug build dashboard subcommand executed successfully!"); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "q_subcommand", feature = "sanity"))] +fn test_q_debug_build_autocomplete_switch() -> Result<(), Box> { + println!("\n๐Ÿ” Testing q debug build autocomplete switch functionality... | Description: Tests the q debug build autocomplete <build> subcommand to switch between different autocomplete builds and revert back."); + + let builds = ["production", "beta"]; + + // Get current build + println!("\n๐Ÿ” Getting current build..."); + let current_response = q_chat_helper::execute_q_subcommand("q", &["debug", "build", "autocomplete"])?; + let current_build = current_response.split_whitespace().last().unwrap_or("production"); + + println!("๐Ÿ“ Build response: {} bytes", current_response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", current_response); + println!("๐Ÿ“ END OUTPUT"); + + // Find any different build from the array + let other_build = builds.iter().find(|&&b| b != current_build) + .unwrap_or(&"beta"); // fallback to beta if current not found in array + + + // Switch to other build + println!("\n๐Ÿ” Switching to {} build...", other_build); + let switch_response = q_chat_helper::execute_q_subcommand("q", &["debug", "build", "autocomplete", other_build])?; + + println!("๐Ÿ“ Switch response: {} bytes", switch_response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", switch_response); + println!("๐Ÿ“ END OUTPUT"); + + assert!(switch_response.contains("Amazon Q") && switch_response.contains(other_build) && switch_response.contains("autocomplete")); + println!("โœ… Switched to {} build successfully!", other_build); + + // Switch back to original build + println!("\n๐Ÿ” Switching back to {} build...", current_build); + let revert_response = q_chat_helper::execute_q_subcommand("q", &["debug", "build", "autocomplete", current_build])?; + + println!("๐Ÿ“ Switching back response: {} bytes", revert_response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", revert_response); + println!("๐Ÿ“ END OUTPUT"); + + assert!(revert_response.contains("Amazon Q") && revert_response.contains(current_build) && revert_response.contains("autocomplete")); + println!("โœ… Switched back to {} build successfully!", current_build); + + println!("โœ… Build switching test completed successfully!"); + + Ok(()) +} \ No newline at end of file diff --git a/e2etests/tests/q_subcommand/test_q_doctor_subcommand.rs b/e2etests/tests/q_subcommand/test_q_doctor_subcommand.rs new file mode 100644 index 0000000000..43afb94cbe --- /dev/null +++ b/e2etests/tests/q_subcommand/test_q_doctor_subcommand.rs @@ -0,0 +1,27 @@ +#[allow(unused_imports)] +use q_cli_e2e_tests::q_chat_helper; + +#[test] +#[cfg(all(feature = "q_subcommand", feature = "sanity"))] +fn test_q_doctor_subcommand() -> Result<(), Box> { + println!("\n๐Ÿ” Testing q doctor subcommand... | Description: Tests the q doctor subcommand that debugs installation issues"); + + println!("\n๐Ÿ” Executing 'q doctor' subcommand..."); + let response = q_chat_helper::execute_q_subcommand("q", &["doctor"])?; + + println!("๐Ÿ“ Doctor response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + assert!(response.contains("q issue"), "Missing troubleshooting message"); + println!("โœ… Found troubleshooting message"); + + if response.contains("Everything looks good!") { + println!("โœ… Doctor check passed - everything looks good!"); + } + + println!("โœ… Doctor subcommand output verified!"); + + Ok(()) +} diff --git a/e2etests/tests/q_subcommand/test_q_inline_subcommand.rs b/e2etests/tests/q_subcommand/test_q_inline_subcommand.rs new file mode 100644 index 0000000000..8ee3865f15 --- /dev/null +++ b/e2etests/tests/q_subcommand/test_q_inline_subcommand.rs @@ -0,0 +1,305 @@ +#[allow(unused_imports)] +use q_cli_e2e_tests::q_chat_helper; + +#[test] +#[cfg(all(feature = "q_subcommand", feature = "sanity"))] +fn test_q_inline_subcommand() -> Result<(), Box> { + println!("\n๐Ÿ” Testing q inline subcommand... | Description: Tests the q inline subcommand for inline shell completion"); + + println!("\n๐Ÿ” Executing 'q inline' subcommand..."); + let response = q_chat_helper::execute_q_subcommand("q", &["inline"])?; + + println!("๐Ÿ“ Debug response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Assert that q inline shows inline shell completions help + assert!(response.contains("Inline shell completions"), "Response should contain 'Inline shell completions'"); + assert!(response.contains("enable"), "Response should show 'enable' command"); + assert!(response.contains("disable"), "Response should show 'disable' command"); + assert!(response.contains("status"), "Response should show 'status' command"); + + println!("โœ… q inline subcommand executed successfully!"); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "q_subcommand", feature = "sanity"))] +fn test_q_inline_help_subcommand() -> Result<(), Box> { + println!("\n๐Ÿ” Testing q inline --help subcommand... | Description: Tests the q inline --help subcommand for inline shell completion"); + + println!("\n๐Ÿ” Executing 'q inline --help' subcommand..."); + let response = q_chat_helper::execute_q_subcommand_with_stdin("q", &["inline"], Some("--help"))?; + + println!("๐Ÿ“ Debug response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Assert that q inline shows inline shell completions help + assert!(response.contains("Inline shell completions"), "Response should contain 'Inline shell completions'"); + assert!(response.contains("enable"), "Response should show 'enable' command"); + assert!(response.contains("disable"), "Response should show 'disable' command"); + assert!(response.contains("status"), "Response should show 'status' command"); + + println!("โœ… q inline help subcommand executed successfully!"); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "q_subcommand", feature = "sanity"))] +fn test_q_inline_disable_subcommand() -> Result<(), Box> { + println!("\n๐Ÿ” Testing q inline disable subcommand... | Description: Tests the q inline disable subcommand for disabling inline"); + + println!("\n๐Ÿ” Executing 'q inline disable' subcommand..."); + let response = q_chat_helper::execute_q_subcommand("q", &["inline", "disable"])?; + + println!("๐Ÿ“ Debug response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Assert that q inline disable shows success message + assert!(response.contains("Inline disabled"), "Response should contain 'Inline disabled'"); + + println!("โœ… q inline disable subcommand executed successfully!"); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "q_subcommand", feature = "sanity"))] +fn test_q_inline_disable_help_subcommand() -> Result<(), Box> { + println!("\n๐Ÿ” Testing q inline disable --help subcommand... | Description: Tests the q inline disable --help subcommand to show help for disabling inline"); + + println!("\n๐Ÿ” Executing 'q inline disable --help' subcommand..."); + let response = q_chat_helper::execute_q_subcommand("q", &["inline", "disable", "--help"])?; + + println!("๐Ÿ“ Debug response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + assert!(response.contains("q inline disable"), "Response should contain 'q inline disable'"); + + println!("โœ… q inline disable help subcommand executed successfully!"); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "q_subcommand", feature = "sanity"))] +fn test_q_inline_enable_subcommand() -> Result<(), Box> { + println!("\n๐Ÿ” Testing q inline enable subcommand... | Description: Tests the q inline enable subcommand for enabling inline"); + + println!("\n๐Ÿ” Executing 'q inline enable' subcommand..."); + let response = q_chat_helper::execute_q_subcommand("q", &["inline", "enable"])?; + + println!("๐Ÿ“ Debug response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Assert that q inline enable shows success message + assert!(response.contains("Inline enabled"), "Response should contain 'Inline enabled'"); + + println!("โœ… q inline enable subcommand executed successfully!"); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "q_subcommand", feature = "sanity"))] +fn test_q_inline_enable_help_subcommand() -> Result<(), Box> { + println!("\n๐Ÿ” Testing q inline enable --help subcommand... | Description: Tests the q inline enable --help subcommand to show help for enabling inline"); + + println!("\n๐Ÿ” Executing 'q inline enable --help' subcommand..."); + let response = q_chat_helper::execute_q_subcommand("q", &["inline", "enable", "--help"])?; + + println!("๐Ÿ“ Debug response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + assert!(response.contains("q inline enable"), "Response should contain 'q inline enable'"); + + println!("โœ… q inline enable help subcommand executed successfully!"); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "q_subcommand", feature = "sanity"))] +fn test_q_inline_status_subcommand() -> Result<(), Box> { + println!("\n๐Ÿ” Testing q inline status subcommand... | Description: Tests the q inline status subcommand for showing inline status"); + + println!("\n๐Ÿ” Executing 'q inline status' subcommand..."); + let response = q_chat_helper::execute_q_subcommand("q", &["inline", "status"])?; + + println!("๐Ÿ“ Debug response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Assert that q inline status shows available customizations + assert!(response.contains("Inline is enabled"), "Response should contain 'Inline is enabled'"); + + println!("\n๐Ÿ” Executing 'q setting all' subcommand to verify settings..."); + let response = q_chat_helper::execute_q_subcommand("q", &["setting", "all"])?; + + println!("๐Ÿ“ Debug response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + if response.contains("inline.enabled") { + println!("โœ… Verified: inline_enabled is set to true"); + } else { + println!("โŒ Verification failed: inline_enabled is not set to true"); + } + + let response = q_chat_helper::execute_q_subcommand("q", &["settings", "inline.enabled", "--delete"])?; + + println!("๐Ÿ“ Debug response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + assert!(response.contains("Removing") || response.contains("inline.enabled"), "Response should confirm deletion or non-existence of the setting"); + + println!("โœ… q inline status subcommand executed successfully!"); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "q_subcommand", feature = "sanity"))] +fn test_q_inline_status_help_subcommand() -> Result<(), Box> { + println!("\n๐Ÿ” Testing q inline status --help subcommand... | Description: Tests the q inline status --help subcommand to show help for inline status"); + + println!("\n๐Ÿ” Executing 'q inline status --help' subcommand..."); + let response = q_chat_helper::execute_q_subcommand("q", &["inline", "status", "--help"])?; + + println!("๐Ÿ“ Debug response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + assert!(response.contains("q inline status"), "Response should contain 'q inline status'"); + + println!("โœ… q inline status help subcommand executed successfully!"); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "q_subcommand", feature = "sanity"))] +fn test_q_inline_show_customizations_subcommand() -> Result<(), Box> { + println!("\n๐Ÿ” Testing q inline show-customizations subcommand... | Description: Tests the q inline show-customizations that show the available customizations"); + + println!("\n๐Ÿ” Executing 'q inline show-customizations' subcommand..."); + let response = q_chat_helper::execute_q_subcommand("q", &["inline", "show-customizations"])?; + + println!("๐Ÿ“ Debug response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Assert that q inline show-customizations shows available customizations + assert!(response.contains("Amazon-Internal-V1"), "Response should contain 'Amazon-Internal-V1'"); + assert!(response.contains("Amazon-Aladdin-V1"), "Response should contain 'Amazon-Aladdin-V1'"); + + println!("โœ… q inline show-customizations subcommand executed successfully!"); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "q_subcommand", feature = "sanity"))] +fn test_q_inline_show_customizations_help_subcommand() -> Result<(), Box> { + println!("\n๐Ÿ” Testing q inline show-customizations --help subcommand... | Description: Tests the q inline show-customizations --help to show help for showing customizations"); + + println!("\n๐Ÿ” Executing 'q inline show-customizations --help' subcommand..."); + let response = q_chat_helper::execute_q_subcommand("q", &["inline", "show-customizations", "--help"])?; + + println!("๐Ÿ“ Debug response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Assert that q inline show-customizations --help shows available customizations + assert!(response.contains("q inline show-customizations"), "Response should contain 'q inline show-customizations'"); + + println!("โœ… q inline show-customizations --help subcommand executed successfully!"); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "q_subcommand", feature = "sanity"))] +fn test_q_inline_set_customization_subcommand() -> Result<(), Box> { + println!("\n๐Ÿ” Testing q inline set-customization subcommand... | Description: Tests the q inline set-customization interactive menu for selecting customizations"); + + // Use helper function to select second option (Amazon-Internal-V1) + let response = q_chat_helper::execute_interactive_menu_selection("q", &["inline", "set-customization"], 1)?; + + println!("๐Ÿ“ Debug response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Just verify that the command executed (may select first option by default) + assert!(response.contains("Customization") && response.contains("selected"), "Should show selection confirmation"); + + println!("โœ… q inline set-customization subcommand executed successfully!"); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "q_subcommand", feature = "sanity"))] +fn test_q_inline_unset_customization_subcommand() -> Result<(), Box> { + println!("\n๐Ÿ” Testing q inline unset customization... | Description: Tests the q inline set-customization interactive menu for selecting 'None' to unset customization"); + + // Get the interactive menu to find None position (always at last line) + let menu_response = q_chat_helper::execute_q_subcommand("q", &["inline", "set-customization"])?; + let none_index = menu_response.lines().count(); + + + let response = q_chat_helper::execute_interactive_menu_selection("q", &["inline", "set-customization"], none_index)?; + + println!("๐Ÿ“ Debug response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify that None was selected (customization unset) + assert!(response.contains("Customization") && response.contains("unset"), "Should show None selection or unset confirmation"); + + println!("โœ… q inline unset customization executed successfully!"); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "q_subcommand", feature = "sanity"))] +fn test_q_inline_set_customization_help_subcommand() -> Result<(), Box> { + println!("\n๐Ÿ” Testing q inline set-customization --help subcommand... | Description: Tests the q inline set-customization --help to show help for setting customizations"); + + let response = q_chat_helper::execute_q_subcommand("q", &["inline", "set-customization", "--help"])?; + + println!("๐Ÿ“ Debug response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Assert that q inline set-customization --help shows available customizations + assert!(response.contains("q inline set-customization"), "Response should contain 'set-customization'"); + + println!("โœ… q inline set-customization --help subcommand executed successfully!"); + + Ok(()) +} \ No newline at end of file diff --git a/e2etests/tests/q_subcommand/test_q_quit_subcommand.rs b/e2etests/tests/q_subcommand/test_q_quit_subcommand.rs new file mode 100644 index 0000000000..4e246995b5 --- /dev/null +++ b/e2etests/tests/q_subcommand/test_q_quit_subcommand.rs @@ -0,0 +1,31 @@ +#[allow(unused_imports)] +use q_cli_e2e_tests::q_chat_helper; + +#[test] +#[cfg(all(feature = "q_subcommand", feature = "sanity"))] +fn test_q_quit_subcommand() -> Result<(), Box> { + println!( + "\n๐Ÿ” Testing q settings q quit subcommand | Description: Tests the q quit subcommand to validate whether it quit the amazon q app." + ); + // Launch Amazon Q app. + println!("Launching Q..."); + let launch_response = q_chat_helper::execute_q_subcommand("q", &["launch"])?; + println!("๐Ÿ“ Debug response: {} bytes", launch_response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", launch_response); + println!("๐Ÿ“ END OUTPUT"); + + assert!(launch_response.contains("Opening Amazon Q dashboard"),"Missing amazon Q opening message"); + + // Quit Amazon q app. + println!("Quitting Q..."); + let quit_response = q_chat_helper::execute_q_subcommand("q", &["quit"])?; + println!("๐Ÿ“ Debug response: {} bytes", quit_response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", quit_response); + println!("๐Ÿ“ END OUTPUT"); + + assert!(quit_response.contains("Quitting Amazon Q app"), "Missing amazon Q quit message"); + Ok(()) + +} \ No newline at end of file diff --git a/e2etests/tests/q_subcommand/test_q_restart_subcommand.rs b/e2etests/tests/q_subcommand/test_q_restart_subcommand.rs new file mode 100644 index 0000000000..7943240049 --- /dev/null +++ b/e2etests/tests/q_subcommand/test_q_restart_subcommand.rs @@ -0,0 +1,25 @@ +#[allow(unused_imports)] +use q_cli_e2e_tests::q_chat_helper; + +/// Tests the q restart subcommand +#[test] +#[cfg(all(feature = "q_subcommand", feature = "sanity"))] +fn test_q_restart_subcommand() -> Result<(), Box> { + println!("\n๐Ÿ” Testing q restart subcommand... | Description: Tests the q restart subcommand to restart Amazon Q."); + + println!("\n๐Ÿ› ๏ธ Running 'q restart' subcommand..."); + let response = q_chat_helper::execute_q_subcommand("q", &["restart"])?; + + println!("๐Ÿ“ Restart response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Validate output contains expected restart messages + assert!(response.contains("Restart") || response.contains("Launching"), "Should contain 'Restarting Amazon Q' OR 'Launching Amazon Q'"); + assert!(response.contains("Open"), "Should contain 'Opening Amazon Q dashboard'"); + + println!("โœ… Amazon Q restart executed successfully!"); + + Ok(()) +} \ No newline at end of file diff --git a/e2etests/tests/q_subcommand/test_q_setting_subcommand.rs b/e2etests/tests/q_subcommand/test_q_setting_subcommand.rs new file mode 100644 index 0000000000..a2e3e963c0 --- /dev/null +++ b/e2etests/tests/q_subcommand/test_q_setting_subcommand.rs @@ -0,0 +1,102 @@ +#[allow(unused_imports)] +use q_cli_e2e_tests::q_chat_helper; + +/// Tests the q settings --help subcommand +#[test] +#[cfg(all(feature = "q_subcommand", feature = "sanity"))] +fn test_q_setting_help_subcommand() -> Result<(), Box> { + println!("\n๐Ÿ” Testing q settings --help subcommand... | Description: Tests the q settings --help subcommand to validate help output format and content."); + + println!("\n๐Ÿ› ๏ธ Running 'q settings --help' subcommand..."); + let response = q_chat_helper::execute_q_subcommand("q", &["settings", "--help"])?; + + println!("๐Ÿ“ Help response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Validate help output contains expected sections + assert!(response.contains("Usage:") && response.contains("q settings") && response.contains("[OPTIONS]") && response.contains("[KEY]") && response.contains("[VALUE]") && response.contains(""), + "Help should contain usage line"); + assert!(response.contains("Commands:"), + "Help should contain commands section"); + assert!(response.contains("open") && response.contains("list") && response.contains("help"), + "Help should contain all subcommands related to q setting subcommand"); + assert!(response.contains("Arguments:"), + "Help should contain Arguments section"); + assert!(response.contains("Options:"), + "Help should contain Options section"); + assert!(response.contains("-d, --delete"), + "Help should contain delete option"); + assert!(response.contains("-f, --format "), + "Help should contain format option"); + assert!(response.contains("-v, --verbose"), + "Help should contain verbose option"); + assert!(response.contains("-h, --help"), + "Should contain help option"); + println!("โœ… Help output validated successfully!"); + + Ok(()) +} + +/// Tests the q setting all subcommand +#[test] +#[cfg(all(feature = "q_subcommand", feature = "sanity"))] +fn test_q_settings_all_subcommand() -> Result<(), Box> { + println!("\n๐Ÿ” Testing q settings all subcommand... | Description: Tests the q settings all subcommand to display all settings."); + + println!("\n๐Ÿ› ๏ธ Running 'q settings all' subcommand..."); + let response = q_chat_helper::execute_q_subcommand("q", &["settings", "all"])?; + + println!("๐Ÿ“ All settings response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Validate output contains expected settings + assert!(response.contains("chat.defaultAgent"), "Should contain chat.defaultAgent setting"); + assert!(response.len() > 10, "Response should be substantial"); + + println!("โœ… All settings displayed successfully!"); + + Ok(()) +} + +/// Tests the q settings help subcommand +#[test] +#[cfg(all(feature = "q_subcommand", feature = "sanity"))] +fn test_q_settings_help_subcommand() -> Result<(), Box> { + println!("\n๐Ÿ” Testing q settings help subcommand... | Description: Tests the q settings help subcommand to validate help output format and content."); + + println!("\n๐Ÿ› ๏ธ Running 'q settings help' subcommand..."); + let response = q_chat_helper::execute_q_subcommand("q", &["settings", "help"])?; + + println!("๐Ÿ“ Help response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Validate help output contains expected sections + assert!(response.contains("Usage:") && response.contains("q settings") && response.contains("[OPTIONS]") && response.contains("[KEY]") && response.contains("[VALUE]") && response.contains(""), + "Help should contain usage line"); + assert!(response.contains("Commands:"), + "Help should contain commands section"); + assert!(response.contains("open") && response.contains("list") && response.contains("help"), + "Help should contain all subcommands related to q setting subcommand"); + assert!(response.contains("Arguments:"), + "Help should contain Arguments section"); + assert!(response.contains("Options:"), + "Help should contain Options section"); + assert!(response.contains("-d, --delete"), + "Help should contain delete option"); + assert!(response.contains("-f, --format "), + "Help should contain format option"); + assert!(response.contains("-v, --verbose"), + "Help should contain verbose option"); + assert!(response.contains("-h, --help"), + "Should contain help option"); + println!("โœ… Help output validated successfully!"); + + Ok(()) +} + diff --git a/e2etests/tests/q_subcommand/test_q_settings_deletecommand.rs b/e2etests/tests/q_subcommand/test_q_settings_deletecommand.rs new file mode 100644 index 0000000000..d78c40cd64 --- /dev/null +++ b/e2etests/tests/q_subcommand/test_q_settings_deletecommand.rs @@ -0,0 +1,44 @@ +#[allow(unused_imports)] +use q_cli_e2e_tests::q_chat_helper; + +#[test] +#[cfg(all(feature = "q_subcommand", feature = "sanity"))] +fn test_q_setting_delete_subcommand() -> Result<(), Box> { + println!( + "\n๐Ÿ” Testing q settings --delete ... | Description: Tests the q settings --delete subcommand to validate DELETE content." + ); +// Get all the settings + let response = q_chat_helper::execute_q_subcommand("q", &["settings", "list"])?; + + println!("๐Ÿ“ List response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Find first setting (parse key = value format) + for line in response.lines() { + if line.contains(" = ") { + let parts: Vec<&str> = line.split(" = ").collect(); + if parts.len() == 2 { + let key = parts[0].trim(); + let value = parts[1].trim(); + + println!("๐Ÿ“ Found setting: {} = {}", key, value); + + // Delete the setting + let delete_response = q_chat_helper::execute_q_subcommand("q", &["settings", "--delete", key])?; + println!("๐Ÿ“ Delete response: {}", delete_response); + + // Restore the setting + let restore_response = q_chat_helper::execute_q_subcommand("q", &["settings", key, value])?; + println!("๐Ÿ“ Restore response: {}", restore_response); + + assert!(delete_response.contains("Removing"), "Missing delete confirmation"); + break; // Only test first setting + } + } + } + + Ok(()) + +} diff --git a/e2etests/tests/q_subcommand/test_q_settings_format_command.rs b/e2etests/tests/q_subcommand/test_q_settings_format_command.rs new file mode 100644 index 0000000000..3741888e6b --- /dev/null +++ b/e2etests/tests/q_subcommand/test_q_settings_format_command.rs @@ -0,0 +1,26 @@ +#[allow(unused_imports)] +use q_cli_e2e_tests::q_chat_helper; + +/// Tests the 'q settings --format' subcommand with the following: +/// - Verifies that the command returns a non-empty response +/// - Checks that the response contains the expected JSON-formatted setting value +/// - Validates that the setting name is referenced in the output +/// - Uses json-pretty format to display the chat.defaultAgent setting +#[test] +#[cfg(all(feature = "q_subcommand", feature = "sanity"))] +fn test_q_setting_format_subcommand() -> Result<(), Box> { + +println!("\n๐Ÿ” Testing q settings --format ... | Description: Tests the q settings --FORMAT subcommand to validate FORMAT content."); +let response = q_chat_helper::execute_q_subcommand("q", &["settings", "--format", "json-pretty", "chat.defaultAgent"])?; + +println!("๐Ÿ“ transform response: {} bytes", response.len()); +println!("๐Ÿ“ FULL OUTPUT:"); +println!("{}", response); +println!("๐Ÿ“ END OUTPUT"); + +assert!(!response.is_empty(), "Expected non-empty response"); +assert!(response.contains("\"q_cli_default\""), "Expected JSON-formatted setting value"); +assert!(response.contains("chat.defaultAgent"), "Expected command to reference the setting name"); + +Ok(()) +} \ No newline at end of file diff --git a/e2etests/tests/q_subcommand/test_q_translate_subcommand.rs b/e2etests/tests/q_subcommand/test_q_translate_subcommand.rs new file mode 100644 index 0000000000..6c47803387 --- /dev/null +++ b/e2etests/tests/q_subcommand/test_q_translate_subcommand.rs @@ -0,0 +1,26 @@ +#[allow(unused_imports)] +use q_cli_e2e_tests::q_chat_helper; + +#[test] +#[cfg(all(feature = "q_subcommand", feature = "sanity"))] +fn test_q_translate_subcommand() -> Result<(), Box> { + println!("\n๐Ÿ” Testing q translate subcommand... | Description: Tests the q translate subcommand for Natural Language to Shell translation"); + + println!("\n๐Ÿ” Executing 'q translate' subcommand with input 'hello'..."); + + // Use stdin function for translate subcommand + let response = q_chat_helper::execute_q_subcommand_with_stdin("q", &["translate"], Some("hello"))?; + + println!("๐Ÿ“ Translate response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify translation output contains shell subcommand + assert!(response.contains("echo") || response.contains("Shell"), "Missing shell subcommand in translation"); + println!("โœ… Found shell subcommand translation"); + + println!("โœ… Translate subcommand executed successfully!"); + + Ok(()) +} diff --git a/e2etests/tests/q_subcommand/test_q_update_subcommand.rs b/e2etests/tests/q_subcommand/test_q_update_subcommand.rs new file mode 100644 index 0000000000..392ea8635a --- /dev/null +++ b/e2etests/tests/q_subcommand/test_q_update_subcommand.rs @@ -0,0 +1,50 @@ +#[allow(unused_imports)] +use q_cli_e2e_tests::q_chat_helper; +#[allow(unused_imports)] +use regex::Regex; + +/// Tests the q update subcommand +#[test] +#[cfg(all(feature = "q_subcommand", feature = "sanity"))] +fn test_q_update_subcommand() -> Result<(), Box> { + println!("\n๐Ÿ” Testing q update subcommand... | Description: Tests the q update subcommand to check for updates."); + + println!("\n๐Ÿ› ๏ธ Running 'q update' subcommand..."); + let response = q_chat_helper::execute_q_subcommand("q", &["update"])?; + + println!("๐Ÿ“ Update response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Validate output contains expected update information + assert!(response.contains("updates"), "Should contain 'updates'"); + + // Check for version format (e.g., 1.16.2) + let version_regex = Regex::new(r"\d+\.\d+\.\d+")?; + assert!(version_regex.is_match(&response), "Should contain version in format x.y.z"); + + println!("โœ… Update check executed successfully!"); + + Ok(()) +} + +/// Tests the q update -h help flag +#[test] +#[cfg(all(feature = "q_subcommand", feature = "sanity"))] +fn test_q_update_help_flag() -> Result<(), Box> { + println!("\n๐Ÿ” Testing q update -h help flag..."); + + let response = q_chat_helper::execute_q_subcommand("q", &["update", "-h"])?; + + // Verify exact help output format + assert!(response.contains("Usage:") && response.contains("q update") && response.contains("[OPTIONS]"), "Should contain usage line"); + assert!(response.contains("-y, --non-interactive"), "Should contain non-interactive option"); + assert!(response.contains("--relaunch-dashboard"), "Should contain relaunch-dashboard option"); + assert!(response.contains("--rollout"), "Should contain rollout option"); + assert!(response.contains("-v, --verbose..."), "Should contain verbose option"); + assert!(response.contains("-h, --help"), "Should contain help option"); + + println!("โœ… Update help flag test passed!"); + Ok(()) +} \ No newline at end of file diff --git a/e2etests/tests/q_subcommand/test_q_user_subcommand.rs b/e2etests/tests/q_subcommand/test_q_user_subcommand.rs new file mode 100644 index 0000000000..9a978a9f99 --- /dev/null +++ b/e2etests/tests/q_subcommand/test_q_user_subcommand.rs @@ -0,0 +1,55 @@ +#[allow(unused_imports)] +use q_cli_e2e_tests::q_chat_helper; + +/// Tests the q user subcommand +#[test] +#[cfg(all(feature = "q_subcommand", feature = "sanity"))] +fn test_q_user_subcommand() -> Result<(), Box> { + println!("\n๐Ÿ” Testing q user subcommand... | Description: Tests the q user subcommand to display user management help."); + + println!("\n๐Ÿ› ๏ธ Running 'q user' subcommand..."); + let response = q_chat_helper::execute_q_subcommand("q", &["user"])?; + + println!("๐Ÿ“ User response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Validate output contains expected help information + assert!(response.contains("Usage:") && response.contains("user") && response.contains("[OPTIONS]") && response.contains(""), "Should contain usage line"); + assert!(response.contains("Commands:"), "Should contain Commands section"); + assert!(response.contains("login"), "Should contain login command"); + assert!(response.contains("logout"), "Should contain logout command"); + assert!(response.contains("whoami"), "Should contain whoami command"); + assert!(response.contains("profile"), "Should contain profile command"); + assert!(response.contains("Options:"), "Should contain Options section"); + assert!(response.contains("-v, --verbose"), "Should contain verbose option"); + assert!(response.contains("-h, --help"), "Should contain help option"); + + println!("โœ… User command help displayed successfully!"); + + Ok(()) +} + +/// Tests the q user -h help flag +#[test] +#[cfg(all(feature = "q_subcommand", feature = "sanity"))] +fn test_q_user_help_flag() -> Result<(), Box> { + println!("\n๐Ÿ” Testing q user -h help flag..."); + + let response = q_chat_helper::execute_q_subcommand("q", &["user", "-h"])?; + + // Validate output contains expected help information + assert!(response.contains("Usage:") && response.contains("user") && response.contains("[OPTIONS]") && response.contains(""), "Should contain usage line"); + assert!(response.contains("Commands:"), "Should contain Commands section"); + assert!(response.contains("login"), "Should contain login command"); + assert!(response.contains("logout"), "Should contain logout command"); + assert!(response.contains("whoami"), "Should contain whoami command"); + assert!(response.contains("profile"), "Should contain profile command"); + assert!(response.contains("Options:"), "Should contain Options section"); + assert!(response.contains("-v, --verbose"), "Should contain verbose option"); + assert!(response.contains("-h, --help"), "Should contain help option"); + + println!("โœ… User help flag test passed!"); + Ok(()) +} \ No newline at end of file diff --git a/e2etests/tests/q_subcommand/test_q_whoami_subcommand.rs b/e2etests/tests/q_subcommand/test_q_whoami_subcommand.rs new file mode 100644 index 0000000000..2bb95c5069 --- /dev/null +++ b/e2etests/tests/q_subcommand/test_q_whoami_subcommand.rs @@ -0,0 +1,133 @@ +#[allow(unused_imports)] +use q_cli_e2e_tests::q_chat_helper; + +/// Tests the q whoami subcommand +#[test] +#[cfg(all(feature = "q_subcommand", feature = "sanity"))] +fn test_q_whoami_subcommand() -> Result<(), Box> { + println!("\n๐Ÿ” Testing q whoami subcommand... | Description: Tests the q whoami subcommand to display user profile information."); + + println!("\n๐Ÿ› ๏ธ Running 'q whoami' subcommand..."); + let response = q_chat_helper::execute_q_subcommand("q", &["whoami"])?; + + println!("๐Ÿ“ Whoami response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Validate output contains expected authentication information + assert!(response.contains("Logged"), "Should contain IAM Identity Center login info"); + assert!(response.contains("Profile:"), "Should contain Profile section"); + + println!("โœ… Whoami information displayed successfully!"); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "q_subcommand", feature = "sanity"))] +fn test_q_whoami_help_subcommand() -> Result<(), Box> { + println!("\n๐Ÿ” Testing q whoami --help subcommand... | Description: Tests the q whoami --help subcommand to validate help output format and content."); + + println!("\n๐Ÿ” Executing 'q whoami --help' subcommand..."); + let response = q_chat_helper::execute_q_subcommand("q", &["whoami", "--help"])?; + + println!("๐Ÿ“ whoami response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Assert whoami help output contains expected commands + assert!(response.contains("Usage:") && response.contains("q whoami") && response.contains("[OPTIONS]"), + "Help should contain usage line"); + assert!(response.contains("Options:"), "Help should contain Options section"); + assert!(response.contains("-f, --format"), "Help should contain format option"); + assert!(response.contains("-v, --verbose"), "Help should contain verbose option"); + assert!(response.contains("-h, --help"), "Should contain help option"); + + println!("โœ… Got whoami help output ({} bytes)!", response.len()); + println!("โœ… q whoami --help subcommand executed successfully!"); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "q_subcommand", feature = "sanity"))] +fn test_q_whoami_f_plain_subcommand() -> Result<(), Box> { + println!("\n๐Ÿ” Testing q whoami -f plain subcommand... | Description: Tests the q whoami -f plain subcommand to display user profile information in plain format."); + + println!("\n๐Ÿ› ๏ธ Running 'q whoami -f plain' subcommand..."); + let response = q_chat_helper::execute_q_subcommand("q", &["whoami", "-f", "plain"])?; + + println!("๐Ÿ“ Whoami response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Validate output contains expected authentication information + assert!(response.contains("Logged"), "Should contain IAM Identity Center login info"); + assert!(response.contains("Profile:"), "Should contain Profile section"); + + println!("โœ… Whoami information displayed successfully!"); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "q_subcommand", feature = "sanity"))] +fn test_q_whoami_f_json_subcommand() -> Result<(), Box> { + println!("\n๐Ÿ” Testing q whoami -f json subcommand... | Description: Tests the q whoami -f json subcommand to display user profile information in json format."); + + println!("\n๐Ÿ› ๏ธ Running 'q whoami -f json' subcommand..."); + let response = q_chat_helper::execute_q_subcommand("q", &["whoami", "-f", "json"])?; + + println!("๐Ÿ“ Whoami response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Check if accountType and region appear between { and } + let start = response.find('{').unwrap(); + let end = response.rfind('}').unwrap(); + let json_content = &response[start..=end]; + assert!(json_content.contains("accountType")); + assert!(json_content.contains("region")); + + // Validate JSON is single-line format + assert!(!json_content[1..json_content.len()-1].contains('\n'), "JSON should be in single-line format"); + + // Validate output contains expected authentication information + assert!(response.contains("Profile:"), "Should contain Profile section"); + + println!("โœ… Whoami information displayed successfully!"); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "q_subcommand", feature = "sanity"))] +fn test_q_whoami_f_json_pretty_subcommand() -> Result<(), Box> { + println!("\n๐Ÿ” Testing q whoami -f json-pretty subcommand... | Description: Tests the q whoami -f json-pretty subcommand to display user profile information in pretty json format."); + + println!("\n๐Ÿ› ๏ธ Running 'q whoami -f json-pretty' subcommand..."); + let response = q_chat_helper::execute_q_subcommand("q", &["whoami", "-f", "json-pretty"])?; + + println!("๐Ÿ“ Whoami response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Check if accountType and region appear between { and } + let start = response.find('{').unwrap(); + let end = response.rfind('}').unwrap(); + let json_content = &response[start..=end]; + assert!(json_content.contains("accountType")); + assert!(json_content.contains("region")); + + // Validate output contains expected authentication information + assert!(response.contains("Profile:"), "Should contain Profile section"); + + println!("โœ… Whoami information displayed successfully!"); + + Ok(()) +} \ No newline at end of file diff --git a/e2etests/tests/save_load/mod.rs b/e2etests/tests/save_load/mod.rs new file mode 100644 index 0000000000..e7a90d3b4b --- /dev/null +++ b/e2etests/tests/save_load/mod.rs @@ -0,0 +1 @@ +pub mod test_save_load_command; \ No newline at end of file diff --git a/e2etests/tests/save_load/test_save_load_command.rs b/e2etests/tests/save_load/test_save_load_command.rs new file mode 100644 index 0000000000..824c188b98 --- /dev/null +++ b/e2etests/tests/save_load/test_save_load_command.rs @@ -0,0 +1,444 @@ +#[allow(unused_imports)] +use q_cli_e2e_tests::q_chat_helper; + +#[allow(dead_code)] +struct FileCleanup<'a> { + path: &'a str, +} + +impl<'a> Drop for FileCleanup<'a> { + fn drop(&mut self) { + if std::path::Path::new(self.path).exists() { + let _ = std::fs::remove_file(self.path); + println!("โœ… Cleaned up test file"); + } + } +} + +#[test] +#[cfg(all(feature = "save_load", feature = "sanity"))] +fn test_save_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing /save command... | Description: Tests the /save command to export conversation state to a file and verify successful file creation with conversation data"); + + let save_path = "/tmp/qcli_test_save.json"; + let _cleanup = FileCleanup { path: save_path }; + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + // Create actual conversation content + let _help_response = chat.execute_command_with_timeout("/help",Some(2000))?; + let _tools_response = chat.execute_command_with_timeout("/tools",Some(2000))?; + println!("โœ… Created conversation content with /help and /tools commands"); + + // Execute /save command + let response = chat.execute_command_with_timeout(&format!("/save {}", save_path),Some(2000))?; + + println!("๐Ÿ“ Save response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify "Exported conversation state to [file path]" message + assert!(response.contains("Exported") && response.contains(save_path), "Missing export confirmation message"); + println!("โœ… Found expected export message with file path"); + + // Verify file was created and contains expected data + assert!(std::path::Path::new(save_path).exists(), "Save file was not created"); + println!("โœ… Save file created at {}", save_path); + + let file_content = std::fs::read_to_string(save_path)?; + assert!(file_content.contains("help") || file_content.contains("tools"), "File missing expected conversation data"); + println!("โœ… File contains expected conversation data"); + + // Release the lock + drop(chat); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "save_load", feature = "sanity"))] +fn test_save_command_argument_validation() -> Result<(), Box> { + println!("\n๐Ÿ” Testing /save command argument validation... | Description: Tests the /save command without required arguments to verify proper error handling and usage display"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("/save",Some(2000))?; + + println!("๐Ÿ“ Help response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify save error message + assert!(response.contains("error"), "Missing save error message"); + println!("โœ… Found save error message"); + + assert!(response.contains("Usage"), "Missing Usage section"); + assert!(response.contains("/save"), "Missing /save command in usage"); + println!("โœ… Found Usage section with /save command"); + + assert!(response.contains("Arguments"), "Missing Arguments section"); + assert!(response.contains(""), "Missing PATH argument"); + println!("โœ… Found Arguments section with PATH parameter"); + + println!("โœ… All help content verified!"); + + // Release the lock + drop(chat); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "save_load", feature = "sanity"))] +fn test_save_help_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing /save --help command... | Description: Tests the /save --help command to display comprehensive help information for save functionality"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("/save --help",Some(2000))?; + + println!("๐Ÿ“ Help response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify save command help content + assert!(response.contains("Save"), "Missing save command description"); + println!("โœ… Found save command description"); + + assert!(response.contains("Usage"), "Missing Usage section"); + assert!(response.contains("/save"), "Missing /save command in usage"); + println!("โœ… Found Usage section with /save command"); + + assert!(response.contains("Arguments"), "Missing Arguments section"); + assert!(response.contains(""), "Missing PATH argument"); + println!("โœ… Found Arguments section with PATH parameter"); + + assert!(response.contains("Options"), "Missing Options section"); + println!("โœ… Found Options section"); + + println!("โœ… All help content verified!"); + + // Release the lock + drop(chat); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "save_load", feature = "sanity"))] +fn test_save_h_flag_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing /save -h command... | Description: Tests the /save -h command (short form) to display save help information"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("/save -h",Some(2000))?; + + println!("๐Ÿ“ Help response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify save command help content + assert!(response.contains("Save"), "Missing save command description"); + println!("โœ… Found save command description"); + + assert!(response.contains("Usage"), "Missing Usage section"); + assert!(response.contains("/save"), "Missing /save command in usage"); + println!("โœ… Found Usage section with /save command"); + + assert!(response.contains("Arguments"), "Missing Arguments section"); + assert!(response.contains(""), "Missing PATH argument"); + println!("โœ… Found Arguments section with PATH parameter"); + + assert!(response.contains("Options"), "Missing Options section"); + println!("โœ… Found Options section"); + + println!("โœ… All help content verified!"); + + // Release the lock + drop(chat); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "save_load", feature = "sanity"))] +fn test_save_force_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing /save --force command... | Description: Tests the /save --force command to overwrite existing files and verify force save functionality"); + + let save_path = "/tmp/qcli_test_save.json"; + let _cleanup = FileCleanup { path: save_path }; + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + // Create actual conversation content + let _help_response = chat.execute_command_with_timeout("/help",Some(2000))?; + let _tools_response = chat.execute_command_with_timeout("/tools",Some(2000))?; + println!("โœ… Created conversation content with /help and /tools commands"); + + // Execute /save command first + let response = chat.execute_command_with_timeout(&format!("/save {}", save_path),Some(2000))?; + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + assert!(response.contains("Exported"), "Initial save should succeed"); + println!("โœ… Initial save completed"); + + // Add more conversation content after initial save + let _prompt_response = chat.execute_command("/context show")?; + println!("โœ… Added more conversation content after initial save"); + + // Execute /save --force command to overwrite with new content + let force_response = chat.execute_command(&format!("/save --force {}", save_path))?; + + println!("๐Ÿ“ Save force response: {} bytes", force_response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", force_response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify force save message + assert!(force_response.contains("Exported") && force_response.contains(save_path), "Missing export confirmation message"); + println!("โœ… Found expected export message with file path"); + + // Verify file exists and contains data + assert!(std::path::Path::new(save_path).exists(), "Save file was not created"); + println!("โœ… Save file created at {}", save_path); + + let file_content = std::fs::read_to_string(save_path)?; + assert!(file_content.contains("help") || file_content.contains("tools"), "File missing initial conversation data"); + assert!(file_content.contains("context"), "File missing additional conversation data"); + println!("โœ… File contains expected conversation data including additional content"); + + // Release the lock + drop(chat); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "save_load", feature = "sanity"))] +fn test_save_f_flag_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing /save -f command... | Description: Tests the /save -f command (short form) to force overwrite existing files"); + + let save_path = "/tmp/qcli_test_save.json"; + let _cleanup = FileCleanup { path: save_path }; + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + // Create actual conversation content + let _help_response = chat.execute_command_with_timeout("/help",Some(2000))?; + let _tools_response = chat.execute_command_with_timeout("/tools",Some(2000))?; + println!("โœ… Created conversation content with /help and /tools commands"); + + // Execute /save command first + let response = chat.execute_command_with_timeout(&format!("/save {}", save_path),Some(2000))?; + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + assert!(response.contains("Exported"), "Initial save should succeed"); + println!("โœ… Initial save completed"); + + // Add more conversation content after initial save + let _prompt_response = chat.execute_command_with_timeout("/context show",Some(2000))?; + println!("โœ… Added more conversation content after initial save"); + + // Execute /save -f command to overwrite with new content + let force_response = chat.execute_command_with_timeout(&format!("/save -f {}", save_path),Some(2000))?; + + println!("๐Ÿ“ Save force response: {} bytes", force_response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", force_response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify force save message + assert!(force_response.contains("Exported") && force_response.contains(save_path), "Missing export confirmation message"); + println!("โœ… Found expected export message with file path"); + + // Verify file exists and contains data + assert!(std::path::Path::new(save_path).exists(), "Save file was not created"); + println!("โœ… Save file created at {}", save_path); + + let file_content = std::fs::read_to_string(save_path)?; + assert!(file_content.contains("help") || file_content.contains("tools"), "File missing initial conversation data"); + assert!(file_content.contains("context"), "File missing additional conversation data"); + println!("โœ… File contains expected conversation data including additional content"); + + // Release the lock + drop(chat); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "save_load", feature = "sanity"))] +fn test_load_help_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing /load --help command... | Description: Tests the /load --help command to display comprehensive help information for load functionality"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("/load --help",Some(2000))?; + + println!("๐Ÿ“ Help response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify load command help content + assert!(response.contains("Load"), "Missing load command description"); + println!("โœ… Found load command description"); + + assert!(response.contains("Usage"), "Missing Usage section"); + assert!(response.contains("/load"), "Missing /load command in usage"); + println!("โœ… Found Usage section with /load command"); + + assert!(response.contains("Arguments"), "Missing Arguments section"); + assert!(response.contains(""), "Missing PATH argument"); + println!("โœ… Found Arguments section with PATH parameter"); + + assert!(response.contains("Options"), "Missing Options section"); + println!("โœ… Found Options section"); + + println!("โœ… All help content verified!"); + + // Release the lock + drop(chat); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "save_load", feature = "sanity"))] +fn test_load_h_flag_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing /load -h command... | Description: Tests the /load -h command (short form) to display load help information"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("/load -h",Some(2000))?; + + println!("๐Ÿ“ Help response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify load command help content + assert!(response.contains("Load"), "Missing load command description"); + println!("โœ… Found load command description"); + + assert!(response.contains("Usage"), "Missing Usage section"); + assert!(response.contains("/load"), "Missing /load command in usage"); + println!("โœ… Found Usage section with /load command"); + + assert!(response.contains("Arguments"), "Missing Arguments section"); + assert!(response.contains(""), "Missing PATH argument"); + println!("โœ… Found Arguments section with PATH parameter"); + + assert!(response.contains("Options"), "Missing Options section"); + println!("โœ… Found Options section"); + + println!("โœ… All help content verified!"); + + // Release the lock + drop(chat); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "save_load", feature = "sanity"))] +fn test_load_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing /load command... | Description: Tests the /load command to import conversation state from a saved file and verify successful restoration"); + + let save_path = "/tmp/qcli_test_load.json"; + let _cleanup = FileCleanup { path: save_path }; + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + // Create actual conversation content + let _help_response = chat.execute_command_with_timeout("/help",Some(2000))?; + let _tools_response = chat.execute_command_with_timeout("/tools",Some(2000))?; + println!("โœ… Created conversation content with /help and /tools commands"); + + // Execute /save command to create a file to load + let save_response = chat.execute_command_with_timeout(&format!("/save {}", save_path),Some(2000))?; + + println!("๐Ÿ“ Save response: {} bytes", save_response.len()); + println!("๐Ÿ“ SAVE OUTPUT:"); + println!("{}", save_response); + println!("๐Ÿ“ END SAVE OUTPUT"); + + // Verify save was successful + assert!(save_response.contains("Exported") && save_response.contains(save_path), "Missing export confirmation message"); + println!("โœ… Save completed successfully"); + + // Verify file was created + assert!(std::path::Path::new(save_path).exists(), "Save file was not created"); + println!("โœ… Save file created at {}", save_path); + + // Execute /load command to load the saved conversation + let load_response = chat.execute_command_with_timeout(&format!("/load {}", save_path),Some(2000))?; + + println!("๐Ÿ“ Load response: {} bytes", load_response.len()); + println!("๐Ÿ“ LOAD OUTPUT:"); + println!("{}", load_response); + println!("๐Ÿ“ END LOAD OUTPUT"); + + // Verify load was successful + assert!(!load_response.is_empty(), "Load command should return non-empty response"); + assert!(load_response.contains("Imported") && load_response.contains(save_path), "Missing import confirmation message"); + println!("โœ… Load command executed successfully and imported conversation state"); + + // Release the lock + drop(chat); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "save_load", feature = "sanity"))] +fn test_load_command_argument_validation() -> Result<(), Box> { + println!("\n๐Ÿ” Testing /load command argument validation... | Description: Tests the /load command without required arguments to verify proper error handling and usage display"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("/load",Some(2000))?; + + println!("๐Ÿ“ Help response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify load error message + assert!(response.contains("error"), "Missing load error message"); + println!("โœ… Found load error message"); + + assert!(response.contains("Usage"), "Missing Usage section"); + assert!(response.contains("/load"), "Missing /load command in usage"); + println!("โœ… Found Usage section with /load command"); + + assert!(response.contains("Arguments"), "Missing Arguments section"); + assert!(response.contains(""), "Missing PATH argument"); + println!("โœ… Found Arguments section with PATH parameter"); + + assert!(response.contains("Options"), "Missing Options section"); + println!("โœ… Found Options section"); + + println!("โœ… All help content verified!"); + + // Release the lock + drop(chat); + + Ok(()) +} \ No newline at end of file diff --git a/e2etests/tests/session_mgmt/mod.rs b/e2etests/tests/session_mgmt/mod.rs new file mode 100644 index 0000000000..ee6705bcc0 --- /dev/null +++ b/e2etests/tests/session_mgmt/mod.rs @@ -0,0 +1,3 @@ +// Module declaration for session_mgmt tests +pub mod test_compact_command; +pub mod test_usage_command; \ No newline at end of file diff --git a/e2etests/tests/session_mgmt/test_compact_command.rs b/e2etests/tests/session_mgmt/test_compact_command.rs new file mode 100644 index 0000000000..65810ba2ee --- /dev/null +++ b/e2etests/tests/session_mgmt/test_compact_command.rs @@ -0,0 +1,482 @@ +#[allow(unused_imports)] +use q_cli_e2e_tests::q_chat_helper; + +#[test] +#[cfg(all(feature = "compact", feature = "sanity"))] +fn test_compact_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing /compact command... | Description: Tests the /compact command to compress conversation history and verify successful compaction or appropriate messaging for short conversations"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("What is AWS explain 100 chaarectors",Some(2000))?; + + println!("๐Ÿ“ AI response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + let response = chat.execute_command_with_timeout("/compact",Some(2000))?; + + println!("๐Ÿ“ Compact response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify compact response - either success or too short + if response.contains("history") && response.contains("compacted") && response.contains("successfully") { + println!("โœ… Found compact success message"); + } else if response.contains("Conversation") && response.contains("short") { + println!("โœ… Found conversation too short message"); + } else { + panic!("Missing expected compact response"); + } + + println!("โœ… All compact content verified!"); + + drop(chat); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "compact", feature = "sanity"))] +fn test_compact_help_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing /compact --help command... | Description: Tests the /compact --help command to display comprehensive help information for conversation compaction functionality"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("/compact --help",Some(2000))?; + + println!("๐Ÿ“ Compact help response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify Usage section + assert!(response.contains("Usage:"), "Missing usage format"); + 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("--show-summary"), "Missing --show-summary option"); + assert!(response.contains("--messages-to-exclude"), "Missing --messages-to-exclude option"); + assert!(response.contains("--truncate-large-messages"), "Missing --truncate-large-messages option"); + assert!(response.contains("--max-message-length"), "Missing --max-message-length option"); + assert!(response.contains("-h") && response.contains("--help"), "Missing -h, --help flags"); + println!("โœ… Found all options and help flags"); + + println!("โœ… All compact help content verified!"); + + drop(chat); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "compact", feature = "sanity"))] +fn test_compact_h_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing /compact -h command... | Description: Tests the /compact -h command (short form) to display compact help information"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("/compact -h",Some(2000))?; + + println!("๐Ÿ“ Compact help response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify Usage section + assert!(response.contains("Usage:"), "Missing usage format"); + 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("--show-summary"), "Missing --show-summary option"); + assert!(response.contains("--messages-to-exclude"), "Missing --messages-to-exclude option"); + assert!(response.contains("--truncate-large-messages"), "Missing --truncate-large-messages option"); + assert!(response.contains("--max-message-length"), "Missing --max-message-length option"); + assert!(response.contains("-h") && response.contains("--help"), "Missing -h, --help flags"); + println!("โœ… Found all options and help flags"); + + println!("โœ… All compact help content verified!"); + + drop(chat); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "compact", feature = "sanity"))] +fn test_compact_truncate_true_command() -> Result<(), Box> { + println!("๐Ÿ” Testing /compact --truncate-large-messages true command... | Description: Test that the /compact โ€”truncate-large-messages true truncates large messages"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("What is AWS explain 100 chaarectors",Some(3000))?; + + println!("๐Ÿ“ AI response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + let response = chat.execute_command_with_timeout("/compact --truncate-large-messages true",Some(3000))?; + + println!("๐Ÿ“ Compact response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + if response.to_lowercase().contains("truncating") { + println!("โœ… Truncation of large messages verified!"); + if response.contains("history") && response.contains("compacted") && response.contains("successfully") { + println!("โœ… Found compact success message"); + } + } else if response.contains("Conversation") && response.contains("short") { + println!("โœ… Found conversation too short message"); + } else { + panic!("Missing expected message"); + } + + println!("โœ… All compact content verified!"); + + drop(chat); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "compact", feature = "sanity"))] +fn test_compact_truncate_false_command() -> Result<(), Box> { + println!("๐Ÿ” Testing /compact --truncate-large-messages false command... | Description: Tests the /compact --truncate-large-messages false command to verify no message truncation occurs"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("What is AWS explain 100 chaarectors",Some(3000))?; + + println!("๐Ÿ“ AI response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + let response = chat.execute_command_with_timeout("/compact --truncate-large-messages false",Some(3000))?; + + println!("๐Ÿ“ Compact response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify compact response - either success or too short + if response.contains("history") && response.contains("compacted") && response.contains("successfully") { + println!("โœ… Found compact success message"); + } else if response.contains("Conversation") && response.contains("short") { + println!("โœ… Found conversation too short message"); + } else { + panic!("Missing expected compact response"); + } + + println!("โœ… All compact content verified!"); + + drop(chat); + + Ok(()) +} + + +#[test] +#[cfg(all(feature = "compact", feature = "sanity"))] +fn test_show_summary() -> Result<(), Box> { + println!("๐Ÿ” Testing /compact --show-summary command... | Description: Tests the /compact --show-summary command to display conversation summary after compaction"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("What is AWS explain 100 chaarectors",Some(2000))?; + + println!("๐Ÿ“ AI response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + let response = chat.execute_command_with_timeout("What is AWS explain 100 chaarectors",Some(2000))?; + + println!("๐Ÿ“ AI response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + let response = chat.execute_command_with_timeout("/compact --show-summary",Some(2000))?; + + println!("๐Ÿ“ Compact response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify compact response - either success or too short + if response.contains("history") && response.contains("compacted") && response.contains("successfully") { + println!("โœ… Found compact success message"); + } else if response.contains("Conversation") && response.contains("short") { + println!("โœ… Found conversation too short message"); + } else { + panic!("Missing expected compact response"); + } + + // Verify compact sumary response + assert!(response.to_lowercase().contains("conversation") && response.to_lowercase().contains("summary"), "Missing Summary section"); + println!("โœ… All compact content verified!"); + + drop(chat); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "compact", feature = "sanity"))] +fn test_max_message_truncate_true() -> Result<(), Box> { + println!("๐Ÿ” Testing /compact --truncate-large-messages true --max-message-length command... | Description: Test /compact --truncate-large-messages true --max-message-length command compacts the conversation by summarizing it to free up context space, truncating large messages to a maximum of provided . "); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("What is AWS explain 100 chaarectors",Some(2000))?; + + println!("๐Ÿ“ AI response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + let response = chat.execute_command_with_timeout("What is DL explain in 100 chrectors",Some(2000))?; + + println!("๐Ÿ“ AI response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + let response = chat.execute_command_with_timeout("/compact --truncate-large-messages true --max-message-length 5",Some(1000))?; + + println!("๐Ÿ“ Compact response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify compact response - either success or too short + if response.to_lowercase().contains("truncating") { + println!("โœ… Truncation of large messages verified!"); + if response.contains("history") && response.contains("compacted") && response.contains("successfully") { + println!("โœ… Found compact success message"); + } + } else if response.contains("Conversation") && response.contains("short") { + println!("โœ… Found conversation too short message"); + } else { + panic!("Missing expected message"); + } + + // Verify compact sumary response + assert!(response.to_lowercase().contains("conversation") && response.to_lowercase().contains("summary"), "Missing Summary section"); + println!("โœ… All compact content verified!"); + + drop(chat); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "compact", feature = "sanity"))] +fn test_max_message_truncate_false() -> Result<(), Box> { + println!("๐Ÿ” Testing /compact --truncate-large-messages false --max-message-length command... | Description: Test /compact --truncate-large-messages false --max-message-length command compacts the conversation by summarizing it to free up context space, but keeps large messages intact (no truncation) despite the max-message-length setting."); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("What is AWS explain 100 chaarectors",Some(2000))?; + + println!("๐Ÿ“ AI response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + let response = chat.execute_command_with_timeout("What is DL explain in 100 chrectors",Some(2000))?; + + println!("๐Ÿ“ AI response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + let response = chat.execute_command_with_timeout("/compact --truncate-large-messages false --max-message-length 5",Some(1000))?; + + println!("๐Ÿ“ Compact response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify compact response - either success or too short + if response.contains("history") && response.contains("compacted") && response.contains("successfully") { + println!("โœ… Found compact success message"); + } else if response.contains("Conversation") && response.contains("short") { + println!("โœ… Found conversation too short message"); + } else { + panic!("Missing expected compact response"); + } + + // Verify compact sumary response + assert!(response.to_lowercase().contains("conversation") && response.to_lowercase().contains("summary"), "Missing Summary section"); + println!("โœ… All compact content verified!"); + + drop(chat); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "compact", feature = "sanity"))] +fn test_max_message_length_invalid() -> Result<(), Box> { + println!("๐Ÿ” Testing /compact --max-message-length command... | Description: Tests the /compact --max-message-length command with invalid subcommand to verify proper error handling and help display"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("What is AWS explain 100 chaarectors",Some(2000))?; + + println!("๐Ÿ“ AI response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + let response = chat.execute_command_with_timeout("What is DL explain in 100 chrectors",Some(2000))?; + + println!("๐Ÿ“ AI response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + let response = chat.execute_command_with_timeout("/compact --max-message-length 5",Some(2000))?; + + println!("๐Ÿ“ Compact response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify error message for missing required argument + assert!(response.contains("error"), "Missing error message"); + assert!(response.contains("--truncate-large-messages") && response.contains("") && response.contains("--max-message-length") && response.contains(""), "Missing required argument info"); + assert!(response.contains("Usage"), "Missing usage info"); + assert!(response.contains("--help"), "Missing help suggestion"); + println!("โœ… Found expected error message for missing --truncate-large-messages argument"); + + drop(chat); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "compact", feature = "sanity"))] +fn test_compact_messages_to_exclude_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing /compact command... | Description: Test /compact --messages-to-exclude command compacts the conversation by summarizing it to free up context space, excluding provided number of user-assistant message pair from the summarization process."); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("What is AWS explain 100 chaarectors",Some(2000))?; + + println!("๐Ÿ“ AI response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + let response = chat.execute_command_with_timeout("What is fibonacci explain in 100 charectors?",Some(2000))?; + + println!("๐Ÿ“ AI response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + let response = chat.execute_command_with_timeout("/compact --messages-to-exclude 1",Some(2000))?; + + println!("๐Ÿ“ Compact response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify compact response - either success or too short + if response.contains("history") && response.contains("compacted") && response.contains("successfully") { + println!("โœ… Found compact success message"); + } else if response.contains("Conversation") && response.contains("short") { + println!("โœ… Found conversation too short message"); + } else { + panic!("Missing expected compact response"); + } + + println!("โœ… All compact content verified!"); + + drop(chat); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "compact", feature = "sanity"))] +fn test_compact_messages_to_exclude_show_sumary_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing /compact command... | Description: Test /compact --messages-to-exclude --show-summary command compacts the conversation by summarizing it to free up context space, excluding provided number of user-assistant message pair from the summarization process and prints the coversation summary."); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + chat.execute_command_with_timeout("/clear",Some(2000))?; + chat.execute_command("y")?; + let response = chat.execute_command_with_timeout("What is AWS explain 100 chaarectors",Some(2000))?; + + println!("๐Ÿ“ AI response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + let response = chat.execute_command_with_timeout("What is fibonacci explain in 100 charectors",Some(2000))?; + + println!("๐Ÿ“ AI response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + let response = chat.execute_command_with_timeout("/compact --messages-to-exclude 1 --show-summary",Some(2000))?; + + println!("๐Ÿ“ Compact response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify compact response - either success or too short + if response.contains("history") && response.contains("compacted") && response.contains("successfully") { + println!("โœ… Found compact success message"); + } else if response.contains("Conversation") && response.contains("short") { + println!("โœ… Found conversation too short message"); + } else { + panic!("Missing expected compact response"); + } + + // Verify compact sumary response + assert!(response.to_lowercase().contains("conversation") && response.to_lowercase().contains("summary"), "Missing Summary section"); + println!("โœ… All compact content verified!"); + + // Verify messages got excluded + assert!(!response.to_lowercase().contains("fibonacci"), "Fibonacci should not be present in compact response"); + println!("โœ… All compact content verified!"); + + println!("โœ… All compact content verified!"); + + drop(chat); + + Ok(()) +} \ No newline at end of file diff --git a/e2etests/tests/session_mgmt/test_usage_command.rs b/e2etests/tests/session_mgmt/test_usage_command.rs new file mode 100644 index 0000000000..ba5cf8dc68 --- /dev/null +++ b/e2etests/tests/session_mgmt/test_usage_command.rs @@ -0,0 +1,139 @@ +#[allow(unused_imports)] +use q_cli_e2e_tests::q_chat_helper; + +/// Tests the /usage command to display current context window usage +/// Verifies token usage information, progress bar, breakdown sections, and Pro Tips +#[test] +#[cfg(all(feature = "usage", feature = "sanity"))] +fn test_usage_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing /usage command... | Description: Tests the /usage command to display current context window usage. Verifies token usage information, progress bar, breakdown sections, and Pro Tips"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("/usage",Some(2000))?; + + println!("๐Ÿ“ Tools response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify context window information + assert!(response.contains("Current context window"), "Missing context window header"); + assert!(response.contains("tokens"), "Missing tokens used information"); + println!("โœ… Found context window and token usage information"); + + // Verify progress bar + assert!(response.contains("%"), "Missing percentage display"); + println!("โœ… Found progress bar with percentage"); + + // Verify token breakdown sections + assert!(response.contains(" Context files:"), "Missing Context files section"); + assert!(response.contains(" Tools:"), "Missing Tools section"); + assert!(response.contains(" Q responses:"), "Missing Q responses section"); + assert!(response.contains(" Your prompts:"), "Missing Your prompts section"); + println!("โœ… Found all token breakdown sections"); + + // Verify token counts and percentages format + assert!(response.contains("tokens ("), "Missing token count format"); + assert!(response.contains("%)"), "Missing percentage format in breakdown"); + println!("โœ… Verified token count and percentage format"); + + // Verify Pro Tips section + assert!(response.contains(" Pro Tips:"), "Missing Pro Tips section"); + println!("โœ… Found Pro Tips section"); + + // Verify specific tip commands + assert!(response.contains("/compact"), "Missing /compact command tip"); + assert!(response.contains("/clear"), "Missing /clear command tip"); + assert!(response.contains("/context show"), "Missing /context show command tip"); + println!("โœ… Found all command tips: /compact, /clear, /context show"); + + println!("โœ… All usage content verified!"); + + println!("โœ… Test completed successfully"); + + drop(chat); + + Ok(()) +} + +// Tests the /usage --help command to display help information for the usage command +// Verifies Usage section, Options section, and help flags (-h, --help) +#[test] +#[cfg(all(feature = "usage", feature = "sanity"))] +fn test_usage_help_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing /usage --help command... | Description: Tests the /usage --help command to display help information for the usage command. Verifies Usage section, Options section, and help flags (-h, --help)"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("/usage --help",Some(2000))?; + + println!("๐Ÿ“ Usage 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("/usage"), "Missing /usage command in usage section"); + println!("โœ… Found Usage section with /usage 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 description"); + + println!("โœ… All usage help content verified!"); + + println!("โœ… Test completed successfully"); + + drop(chat); + + Ok(()) +} + +/// Tests the /usage -h command (short form of --help) +/// Verifies Usage section, Options section, and help flags (-h, --help) +#[test] +#[cfg(all(feature = "usage", feature = "sanity"))] +fn test_usage_h_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing /usage -h command... | Description: Tests the /usage -h command (short form of --help). Verifies Usage section, Options section, and help flags (-h, --help)"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("/usage -h",Some(2000))?; + + println!("๐Ÿ“ Usage 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("/usage"), "Missing /usage command in usage section"); + println!("โœ… Found Usage section with /usage 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 description"); + + println!("โœ… All usage help content verified!"); + + println!("โœ… Test completed successfully"); + + drop(chat); + + Ok(()) +} \ No newline at end of file diff --git a/e2etests/tests/todos/mod.rs b/e2etests/tests/todos/mod.rs new file mode 100644 index 0000000000..2b2628ac75 --- /dev/null +++ b/e2etests/tests/todos/mod.rs @@ -0,0 +1 @@ +pub mod test_todos_command; \ No newline at end of file diff --git a/e2etests/tests/todos/test_todos_command.rs b/e2etests/tests/todos/test_todos_command.rs new file mode 100644 index 0000000000..be752ac325 --- /dev/null +++ b/e2etests/tests/todos/test_todos_command.rs @@ -0,0 +1,429 @@ +#[allow(unused_imports)] +use q_cli_e2e_tests::q_chat_helper; +#[allow(unused_imports)] +use regex::Regex; + +#[test] +#[cfg(all(feature = "todos", feature = "sanity"))] +fn test_todos_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing /todos command... | Description: Tests the /todos command to view, manage, and resume to-do lists"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + println!("โœ… Q Chat session started"); + + let response = chat.execute_command_with_timeout("/todos",Some(2000))?; + + 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("resume"), "Missing resume command"); + assert!(response.contains("view"), "Missing view command"); + assert!(response.contains("delete"), "Missing delete command"); + assert!(response.contains("help"), "Missing help command"); + println!("โœ… Found core commands: resume, view, delete, help"); + + println!("โœ… /todos command test completed successfully"); + + drop(chat); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "todos", feature = "sanity"))] +fn test_todos_help_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing /todos help command... | Description: Tests the /todos help command to display help information about the todos "); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + println!("โœ… Q Chat session started"); + + let response = chat.execute_command_with_timeout("/todos help",Some(2000))?; + + 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("resume"), "Missing resume command"); + assert!(response.contains("view"), "Missing view command"); + assert!(response.contains("delete"), "Missing delete command"); + assert!(response.contains("help"), "Missing help command"); + println!("โœ… Found core commands: resume, view, delete, help"); + + println!("โœ… /todos help command test completed successfully"); + + drop(chat); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "todos", feature = "sanity"))] +fn test_todos_view_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing /todos view command... | Description: Tests the /todos view command to view to-do lists"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + println!("Executing 'q settings chat.enableTodoList true' to enable todos feature..."); + q_chat_helper::execute_q_subcommand("q", &["settings", "chat.enableTodoList", "true"])?; + + let response = q_chat_helper::execute_q_subcommand("q", &["settings", "all"])?; + + println!("๐Ÿ“ Help response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + assert!(response.contains("chat.enableTodoList = true"), "Failed to enable todos feature"); + println!("โœ… Todos feature enabled"); + + println!("โœ… Q Chat session started"); + + let response = chat.execute_command_with_timeout("Add task in todos list Review emails",Some(2000))?; + + println!("๐Ÿ“ Help response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify help content + assert!(response.contains("Using tool"), "Missing tool usage confirmation"); + assert!(response.contains("todo_list"), "Missing todo_list tool usage"); + assert!(response.contains("Review emails"), "Missing Review emails message"); + println!("โœ… Confirmed todo_list tool usage"); + + let response = chat.execute_command_with_timeout("/todos view",Some(2000))?; + + println!("๐Ÿ“ Help response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + assert!(response.contains("to-do"), "Missing to-do message"); + assert!(response.contains("view"), "Missing view message"); + println!("โœ… Confirmed to-do item presence in view output"); + + // Send down arrow to select different model + let selection_response = chat.send_key_input("\x1b[B")?; + + println!("๐Ÿ“ Selection response: {} bytes", selection_response.len()); + println!("๐Ÿ“ SELECTION RESPONSE:"); + println!("{}", selection_response); + println!("๐Ÿ“ END SELECTION RESPONSE"); + + // Send Enter to confirm + let confirm_response = chat.send_key_input("\r")?; + + println!("๐Ÿ“ Confirm response: {} bytes", confirm_response.len()); + println!("๐Ÿ“ CONFIRM RESPONSE:"); + println!("{}", confirm_response); + println!("๐Ÿ“ END CONFIRM RESPONSE"); + + assert!(confirm_response.contains("TODO"), "Missing TODO message"); + assert!(confirm_response.contains("Review emails"), "Missing Review emails to-do item"); + println!("โœ… Confirmed viewing of selected to-do list with items"); + + let response = chat.execute_command_with_timeout("/todos delete",Some(2000))?; + + println!("๐Ÿ“ Help response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + assert!(response.contains("to-do"), "Missing to-do message"); + assert!(response.contains("delete"), "Missing delete message"); + + // Send down arrow to select different model + let selection_response = chat.send_key_input("\x1b[B")?; + + println!("๐Ÿ“ Selection response: {} bytes", selection_response.len()); + println!("๐Ÿ“ SELECTION RESPONSE:"); + println!("{}", selection_response); + println!("๐Ÿ“ END SELECTION RESPONSE"); + + // Send Enter to confirm + let confirm_response = chat.send_key_input("\r")?; + + println!("๐Ÿ“ Confirm response: {} bytes", confirm_response.len()); + println!("๐Ÿ“ CONFIRM RESPONSE:"); + println!("{}", confirm_response); + println!("๐Ÿ“ END CONFIRM RESPONSE"); + + assert!(confirm_response.contains("Deleted"), "Missing Deleted message"); + assert!(confirm_response.contains("to-do"), "Missing to-do item"); + println!("โœ… Confirmed deletion of selected to-do list"); + + println!("โœ… /todos view command test completed successfully"); + + drop(chat); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "todos", feature = "sanity"))] +fn test_todos_resume_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing /todos resume command... | Description: Tests the /todos resume command to resume a specific to-do list"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + println!("Executing 'q settings chat.enableTodoList true' to enable todos feature..."); + q_chat_helper::execute_q_subcommand("q", &["settings", "chat.enableTodoList", "true"])?; + + let response = q_chat_helper::execute_q_subcommand("q", &["settings", "all"])?; + + println!("๐Ÿ“ Help response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + assert!(response.contains("chat.enableTodoList = true"), "Failed to enable todos feature"); + println!("โœ… Todos feature enabled"); + + println!("โœ… Q Chat session started"); + + let response = chat.execute_command_with_timeout("Add task in todos list Review emails",Some(2000))?; + + println!("๐Ÿ“ Help response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify help content + assert!(response.contains("Using tool"), "Missing tool usage confirmation"); + assert!(response.contains("todo_list"), "Missing todo_list tool usage"); + assert!(response.contains("Review emails"), "Missing Review emails message"); + println!("โœ… Confirmed todo_list tool usage"); + + let response = chat.execute_command_with_timeout("/todos resume",Some(2000))?; + + println!("๐Ÿ“ Help response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + assert!(response.contains("to-do"), "Missing to-do message"); + assert!(response.contains("resume"), "Missing resume message"); + println!("โœ… Confirmed to-do item presence in resume output"); + + // Send down arrow to select different model + let selection_response = chat.send_key_input("\x1b[B")?; + + println!("๐Ÿ“ Selection response: {} bytes", selection_response.len()); + println!("๐Ÿ“ SELECTION RESPONSE:"); + println!("{}", selection_response); + println!("๐Ÿ“ END SELECTION RESPONSE"); + + // Send Enter to confirm + let confirm_response = chat.send_key_input("\r")?; + + println!("๐Ÿ“ Confirm response: {} bytes", confirm_response.len()); + println!("๐Ÿ“ CONFIRM RESPONSE:"); + println!("{}", confirm_response); + println!("๐Ÿ“ END CONFIRM RESPONSE"); + + assert!(confirm_response.contains("Review emails"), "Missing Review emails message"); + assert!(confirm_response.contains("TODO"), "Missing TODO item"); + println!("โœ… Confirmed resuming of selected to-do list with items"); + + let response = chat.execute_command_with_timeout("/todos delete",Some(2000))?; + + println!("๐Ÿ“ Help response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + assert!(response.contains("to-do"), "Missing to-do message"); + assert!(response.contains("delete"), "Missing delete message"); + + // Send down arrow to select different model + let selection_response = chat.send_key_input("\x1b[B")?; + + println!("๐Ÿ“ Selection response: {} bytes", selection_response.len()); + println!("๐Ÿ“ SELECTION RESPONSE:"); + println!("{}", selection_response); + println!("๐Ÿ“ END SELECTION RESPONSE"); + + // Send Enter to confirm + let confirm_response = chat.send_key_input("\r")?; + + println!("๐Ÿ“ Confirm response: {} bytes", confirm_response.len()); + println!("๐Ÿ“ CONFIRM RESPONSE:"); + println!("{}", confirm_response); + println!("๐Ÿ“ END CONFIRM RESPONSE"); + + assert!(confirm_response.contains("Deleted"), "Missing Deleted message"); + assert!(confirm_response.contains("to-do"), "Missing to-do item"); + println!("โœ… Confirmed deletion of selected to-do list"); + + println!("โœ… /todos resume command test completed successfully"); + + drop(chat); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "todos", feature = "sanity"))] +fn test_todos_delete_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing /todos delete command... | Description: Tests the /todos delete command to delete a specific to-do list"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + println!("Executing 'q settings chat.enableTodoList true' to enable todos feature..."); + q_chat_helper::execute_q_subcommand("q", &["settings", "chat.enableTodoList", "true"])?; + + let response = q_chat_helper::execute_q_subcommand("q", &["settings", "all"])?; + + println!("๐Ÿ“ Help response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + assert!(response.contains("chat.enableTodoList = true"), "Failed to enable todos feature"); + println!("โœ… Todos feature enabled"); + + println!("โœ… Q Chat session started"); + + let response = chat.execute_command_with_timeout("Add task in todos list Review emails",Some(2000))?; + + println!("๐Ÿ“ Help response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify help content + assert!(response.contains("Using tool"), "Missing tool usage confirmation"); + assert!(response.contains("todo_list"), "Missing todo_list tool usage"); + assert!(response.contains("Review emails"), "Missing Review emails message"); + println!("โœ… Confirmed todo_list tool usage"); + + let response = chat.execute_command_with_timeout("/todos view",Some(2000))?; + + println!("๐Ÿ“ Help response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + assert!(response.contains("to-do"), "Missing to-do message"); + assert!(response.contains("view"), "Missing view message"); + println!("โœ… Confirmed to-do item presence in view output"); + + // Send down arrow to select different model + let selection_response = chat.send_key_input("\x1b[B")?; + + println!("๐Ÿ“ Selection response: {} bytes", selection_response.len()); + println!("๐Ÿ“ SELECTION RESPONSE:"); + println!("{}", selection_response); + println!("๐Ÿ“ END SELECTION RESPONSE"); + + // Send Enter to confirm + let confirm_response = chat.send_key_input("\r")?; + + println!("๐Ÿ“ Confirm response: {} bytes", confirm_response.len()); + println!("๐Ÿ“ CONFIRM RESPONSE:"); + println!("{}", confirm_response); + println!("๐Ÿ“ END CONFIRM RESPONSE"); + + assert!(confirm_response.contains("TODO"), "Missing TODO message"); + assert!(confirm_response.contains("Review emails"), "Missing Review emails to-do item"); + println!("โœ… Confirmed viewing of selected to-do list with items"); + + let response = chat.execute_command_with_timeout("/todos delete",Some(2000))?; + + println!("๐Ÿ“ Help response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + assert!(response.contains("to-do"), "Missing to-do message"); + assert!(response.contains("delete"), "Missing delete message"); + println!("โœ… Confirmed to-do item presence in delete output"); + + // Send down arrow to select different model + let selection_response = chat.send_key_input("\x1b[B")?; + + println!("๐Ÿ“ Selection response: {} bytes", selection_response.len()); + println!("๐Ÿ“ SELECTION RESPONSE:"); + println!("{}", selection_response); + println!("๐Ÿ“ END SELECTION RESPONSE"); + + // Send Enter to confirm + let confirm_response = chat.send_key_input("\r")?; + + println!("๐Ÿ“ Confirm response: {} bytes", confirm_response.len()); + println!("๐Ÿ“ CONFIRM RESPONSE:"); + println!("{}", confirm_response); + println!("๐Ÿ“ END CONFIRM RESPONSE"); + + assert!(confirm_response.contains("Deleted"), "Missing Deleted message"); + assert!(confirm_response.contains("to-do"), "Missing to-do item"); + println!("โœ… Confirmed deletion of selected to-do list"); + + println!("โœ… /todos delete command test completed successfully"); + + drop(chat); + + Ok(()) +} +#[test] +#[cfg(all(feature = "todos", feature = "sanity"))] +fn test_todos_clear_finished_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing /todos clear-finished command... | Description: Tests that /todos clear-finished command to validate it clears the todo list."); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + println!("โœ… Global Q Chat session started"); + + // Create todo list with 2 tasks + println!("\n๐Ÿ” Creating todo list with 2 tasks..."); + let create_response = chat.execute_command_with_timeout("create a todo_list with 2 task in amazon q", Some(2000))?; + println!("๐Ÿ“ Create response: {} bytes", create_response.len()); + println!("๐Ÿ“ Create response: {}", create_response); + println!("โœ… Found create response."); + println!("โœ… Tasks has been created successfully."); + + // Extract todo ID + let re = Regex::new(r"(\d{10,})")?; + let todo_id = re.find(&create_response) + .map(|m| m.as_str()) + .ok_or("Could not extract todo list ID")?; + println!("๐Ÿ“ Extracted todo ID: {}", todo_id); + + // Mark all tasks as completed + println!("\n๐Ÿ” Marking all tasks as completed..."); + let mark_response = chat.execute_command_with_timeout(&format!("mark all tasks as completed for todo list {}", todo_id), Some(2000))?; + println!("๐Ÿ“ Mark complete response: {} bytes", mark_response.len()); + println!("๐Ÿ“ Mark complete response: {}", mark_response); + println!("โœ… Found Task completion response."); + + // Test clear-finished command + println!("\n๐Ÿ” Testing clear-finished command..."); + let clear_response = chat.execute_command_with_timeout("/todos clear-finished", Some(2000))?; + println!("๐Ÿ“ Clear response: {} bytes", clear_response.len()); + println!("๐Ÿ“ {}", clear_response); + + assert!(!clear_response.is_empty(), "Expected non-empty response from clear-finished command"); + println!("โœ… Found todo_list clear response"); + println!("โœ… All finished task cleared successfully."); + + drop(chat); + Ok(()) +} \ No newline at end of file diff --git a/e2etests/tests/tools/mod.rs b/e2etests/tests/tools/mod.rs new file mode 100644 index 0000000000..bed96d0878 --- /dev/null +++ b/e2etests/tests/tools/mod.rs @@ -0,0 +1 @@ +pub mod test_tools_command; \ No newline at end of file diff --git a/e2etests/tests/tools/test_tools_command.rs b/e2etests/tests/tools/test_tools_command.rs new file mode 100644 index 0000000000..6576dc6b32 --- /dev/null +++ b/e2etests/tests/tools/test_tools_command.rs @@ -0,0 +1,689 @@ +#[allow(unused_imports)] +use q_cli_e2e_tests::q_chat_helper; + +#[allow(dead_code)] +struct FileCleanup<'a> { + path: &'a str, +} + +impl<'a> Drop for FileCleanup<'a> { + fn drop(&mut self) { + if std::path::Path::new(self.path).exists() { + let _ = std::fs::remove_file(self.path); + println!("โœ… Cleaned up test file"); + } + } +} + +#[test] +#[cfg(all(feature = "tools", feature = "sanity"))] +fn test_tools_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing /tools command... | Description: Tests the /tools command to display all available tools with their permission status including built-in and MCP tools"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("/tools",Some(2000))?; + + println!("๐Ÿ“ Tools response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify tools content structure + assert!(response.contains("Tool"), "Missing Tool header"); + assert!(response.contains("Permission"), "Missing Permission header"); + println!("โœ… Found tools table with Tool and Permission columns"); + + assert!(response.contains("Built-in:"), "Missing Built-in section"); + println!("โœ… Found Built-in tools section"); + + // Verify some expected built-in tools + assert!(response.contains("execute_bash"), "Missing execute_bash tool"); + assert!(response.contains("fs_read"), "Missing fs_read tool"); + assert!(response.contains("fs_write"), "Missing fs_write tool"); + assert!(response.contains("use_aws"), "Missing use_aws tool"); + println!("โœ… Verified core built-in tools: execute_bash, fs_read, fs_write, use_aws"); + + // Check for MCP tools section if present + if response.contains("amzn-mcp (MCP):") { + println!("โœ… Found MCP tools section with Amazon-specific tools"); + assert!(response.contains("not trusted") || response.contains("trusted"), "Missing permission status"); + println!("โœ… Verified permission status indicators (trusted/not trusted)"); + + // Count some MCP tools + let mcp_tools = ["andes", "cradle", "datanet", "read_quip", "taskei_get_task"]; + let found_tools: Vec<&str> = mcp_tools.iter().filter(|&&tool| response.contains(tool)).copied().collect(); + println!("โœ… Found {} MCP tools including: {:?}", found_tools.len(), found_tools); + } + + println!("โœ… All tools content verified!"); + + println!("โœ… /tools command executed successfully"); + + drop(chat); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "tools", feature = "sanity"))] +fn test_tools_help_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing /tools --help command... | Description: Tests the /tools --help command to display comprehensive help information about tools management including available subcommands and options"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("/tools --help",Some(2000))?; + + println!("๐Ÿ“ Tools help response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify Usage section + assert!(response.contains("Usage:") && response.contains("/tools") && response.contains("[COMMAND]"), "Missing Usage section"); + println!("โœ… Found usage format"); + println!("โœ… Found usage format"); + + // Verify Commands section + assert!(response.contains("Commands:"), "Missing Commands section"); + assert!(response.contains("schema"), "Missing schema command"); + assert!(response.contains("trust"), "Missing trust command"); + assert!(response.contains("untrust"), "Missing untrust command"); + assert!(response.contains("trust-all"), "Missing trust-all command"); + assert!(response.contains("reset"), "Missing reset command"); + assert!(response.contains("help"), "Missing help command"); + println!("โœ… Found all commands: schema, trust, untrust, trust-all, reset, help"); + + // Verify Options section + assert!(response.contains("Options:"), "Missing Options section"); + assert!(response.contains("-h") && response.contains("--help"), "Missing -h, --help flags"); + println!("โœ… Found Options section with help flags"); + + println!("โœ… All tools help content verified!"); + + drop(chat); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "tools", feature = "sanity"))] +fn test_tools_trust_all_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing /tools trust-all command... | Description: Tests the /tools trust-all command to trust all available tools and verify all tools show trusted status, then tests reset functionality"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + // Execute trust-all command + let trust_all_response = chat.execute_command_with_timeout("/tools trust-all",Some(2000))?; + + println!("๐Ÿ“ Trust-all response: {} bytes", trust_all_response.len()); + println!("๐Ÿ“ TRUST-ALL OUTPUT:"); + println!("{}", trust_all_response); + println!("๐Ÿ“ END TRUST-ALL OUTPUT"); + + // Verify that all tools now show "trusted" permission + assert!(trust_all_response.contains("All tools") && trust_all_response.contains("trusted"), "Missing trusted tools after trust-all"); + println!("โœ… trust-all confirmation message!!"); + + // Now check tools list to verify all tools are trusted + let tools_response = chat.execute_command_with_timeout("/tools",Some(2000))?; + + println!("๐Ÿ“ Tools response after trust-all: {} bytes", tools_response.len()); + println!("๐Ÿ“ TOOLS OUTPUT:"); + println!("{}", tools_response); + println!("๐Ÿ“ END TOOLS OUTPUT"); + + // Verify that all tools now show "trusted" permission + assert!(tools_response.contains("trusted"), "Missing trusted tools after trust-all"); + + // Verify no tools have other permission statuses + assert!(!tools_response.contains("not trusted"), "Found 'not trusted' tools after trust-all"); + assert!(!tools_response.contains("read-only commands"), "Found 'read-only commands' tools after trust-all"); + println!("โœ… Verified all tools are now trusted, no other permission statuses found"); + + // Count lines with "trusted" to ensure multiple tools are trusted + let trusted_count = tools_response.matches("trusted").count(); + assert!(trusted_count > 0, "No trusted tools found"); + println!("โœ… Found {} instances of 'trusted' in tools list", trusted_count); + + println!("โœ… All tools trust-all functionality verified!"); + + // Execute reset command + let reset_response = chat.execute_command_with_timeout("/tools reset",Some(1000))?; + + println!("๐Ÿ“ Reset response: {} bytes", reset_response.len()); + println!("๐Ÿ“ RESET OUTPUT:"); + println!("{}", reset_response); + println!("๐Ÿ“ END RESET OUTPUT"); + + // Verify reset confirmation message + assert!(reset_response.contains("Reset") && reset_response.contains("permission"), "Missing reset confirmation message"); + println!("โœ… Found reset confirmation message"); + + // Now check tools list to verify tools have mixed permissions + let tools_response = chat.execute_command_with_timeout("/tools",Some(2000))?; + + println!("๐Ÿ“ Tools response after reset: {} bytes", tools_response.len()); + println!("๐Ÿ“ TOOLS OUTPUT:"); + println!("{}", tools_response); + println!("๐Ÿ“ END TOOLS OUTPUT"); + + // Verify that tools have all permission types + assert!(tools_response.contains("trusted"), "Missing trusted tools"); + assert!(tools_response.contains("not trusted"), "Missing not trusted tools"); + assert!(tools_response.contains("read-only commands"), "Missing read-only commands tools"); + println!("โœ… Found all permission types after reset"); + + println!("โœ… All tools reset functionality verified!"); + + drop(chat); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "tools", feature = "sanity"))] +fn test_tools_trust_all_help_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing /tools trust-all --help command... | Description: Tests the /tools trust-all --helpcommand to display help information for the trust-all subcommand"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("/tools trust-all --help",Some(2000))?; + + println!("๐Ÿ“ Tools trust-all help response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + + // Verify usage format + assert!(response.contains("Usage:") && response.contains("/tools trust-all"), "Missing usage format"); + println!("โœ… Found usage format"); + + // Verify options section + assert!(response.contains("Options:"), "Missing Options section"); + assert!(response.contains("-h") && response.contains("--help"), "Missing help option"); + println!("โœ… Found options section with help flag"); + + println!("โœ… All tools trust-all help functionality verified!"); + + drop(chat); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "tools", feature = "sanity"))] +fn test_tools_reset_help_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing /tools reset --help command... | Description: Tests the /tools reset --help command to display help information for the reset subcommand"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("/tools reset --help",Some(2000))?; + + println!("๐Ÿ“ Tools reset help response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify usage format + assert!(response.contains("Usage:") && response.contains("/tools reset"), "Missing usage format"); + println!("โœ… Found usage format"); + + // Verify options section + assert!(response.contains("Options:"), "Missing Options section"); + assert!(response.contains("-h") && response.contains("--help"), "Missing help option"); + println!("โœ… Found options section with help flag"); + + println!("โœ… All tools reset help functionality verified!"); + + drop(chat); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "tools", feature = "sanity"))] +fn test_tools_trust_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing /tools trust command... | Description: Tests the /tools trust and untrust commands to manage individual tool permissions and verify trust status changes"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + // First get list of tools to find one that's not trusted + let tools_response = chat.execute_command_with_timeout("/tools",Some(2000))?; + + println!("๐Ÿ“ Tools response: {} bytes", tools_response.len()); + println!("๐Ÿ“ TOOLS OUTPUT:"); + println!("{}", tools_response); + println!("๐Ÿ“ END TOOLS OUTPUT"); + + // Find a tool that's not trusted + let mut untrusted_tool: Option = None; + + // Look for tools that are "not trusted" + let lines: Vec<&str> = tools_response.lines().collect(); + for line in lines { + if line.starts_with("- ") && line.contains("not trusted") { + // Extract tool name from the line (after "- ") + if let Some(tool_part) = line.strip_prefix("- ") { + let parts: Vec<&str> = tool_part.split_whitespace().collect(); + if let Some(tool_name) = parts.first() { + untrusted_tool = Some(tool_name.to_string()); + break; + } + } + } + } + + if let Some(tool_name) = untrusted_tool { + println!("โœ… Found untrusted tool: {}", tool_name); + + // Execute trust command + let trust_command = format!("/tools trust {}", tool_name); + let trust_response = chat.execute_command_with_timeout(&trust_command,Some(2000))?; + + println!("๐Ÿ“ Trust response: {} bytes", trust_response.len()); + println!("๐Ÿ“ TRUST OUTPUT:"); + println!("{}", trust_response); + println!("๐Ÿ“ END TRUST OUTPUT"); + + // Verify trust confirmation message + assert!(trust_response.contains(&tool_name), "Missing trust confirmation message"); + println!("โœ… Found trust confirmation message for tool: {}", tool_name); + + // Execute untrust command + let untrust_command = format!("/tools untrust {}", tool_name); + let untrust_response = chat.execute_command_with_timeout(&untrust_command,Some(2000))?; + + println!("๐Ÿ“ Untrust response: {} bytes", untrust_response.len()); + println!("๐Ÿ“ UNTRUST OUTPUT:"); + println!("{}", untrust_response); + println!("๐Ÿ“ END UNTRUST OUTPUT"); + + // Verify untrust confirmation message + let expected_untrust_message = format!("Tool '{}' is", tool_name); + assert!(untrust_response.contains(&expected_untrust_message), "Missing untrust confirmation message"); + println!("โœ… Found untrust confirmation message for tool: {}", tool_name); + + println!("โœ… All tools trust/untrust functionality verified!"); + } else { + println!("โ„น๏ธ No untrusted tools found to test trust command"); + } + + drop(chat); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "tools", feature = "sanity"))] +fn test_tools_trust_help_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing /tools trust --help command... | Description: Tests the /tools trust --help command to display help information for trusting specific tools"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("/tools trust --help",Some(2000))?; + + println!("๐Ÿ“ Tools trust help response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify usage format + assert!(response.contains("Usage:") && response.contains("/tools trust") && response.contains(""), "Missing usage format"); + println!("โœ… Found usage format"); + + // Verify arguments section + assert!(response.contains("Arguments:") && response.contains(""), "Missing Arguments section"); + println!("โœ… Found arguments section"); + + // Verify options section + assert!(response.contains("Options:"), "Missing Options section"); + assert!(response.contains("-h") && response.contains("--help"), "Missing help option"); + println!("โœ… Found options section with help flag"); + + println!("โœ… All tools trust help functionality verified!"); + + drop(chat); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "tools", feature = "sanity"))] +fn test_tools_untrust_help_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing /tools untrust --help command... | Description: Tests the /tools untrust --help command to display help information for untrusting specific tools"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("/tools untrust --help",Some(2000))?; + + println!("๐Ÿ“ Tools untrust help response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify usage format + assert!(response.contains("Usage:") && response.contains("/tools untrust") && response.contains(""), "Missing usage format"); + println!("โœ… Found usage format"); + + // Verify arguments section + assert!(response.contains("Arguments:") && response.contains(""), "Missing Arguments section"); + println!("โœ… Found arguments section"); + + // Verify options section + assert!(response.contains("Options:"), "Missing Options section"); + assert!(response.contains("-h") && response.contains("--help"), "Missing help option"); + println!("โœ… Found options section with help flag"); + + println!("โœ… All tools untrust help functionality verified!"); + + drop(chat); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "tools", feature = "sanity"))] +fn test_tools_schema_help_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing /tools schema --help command... | Description: Tests the /tools schema --help command to display help information for viewing tool schemas"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + let response = chat.execute_command_with_timeout("/tools schema --help",Some(2000))?; + + println!("๐Ÿ“ Tools schema help response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify usage format + assert!(response.contains("Usage:") && response.contains("/tools schema"), "Missing usage format"); + println!("โœ… Found usage format"); + + // Verify options section + assert!(response.contains("Options:"), "Missing Options section"); + assert!(response.contains("-h") && response.contains("--help"), "Missing help option"); + println!("โœ… Found options section with help flag"); + + println!("โœ… All tools schema help functionality verified!"); + + drop(chat); + + Ok(()) +} +//TODO: As response not giving full content , need to check this. +/*#[test] +#[cfg(feature = "tools")] +fn test_tools_schema_command() -> Result<(), Box> { + println!("\n๐Ÿ” Testing /tools schema command..."); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + let response = chat.execute_command("/tools schema")?; + + println!("๐Ÿ“ Tools schema response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify JSON structure + assert!(response.contains("{") && response.contains("}"), "Missing JSON structure"); + println!("โœ… Found JSON structure"); + + // Verify core built-in tools + assert!(response.contains("fs_read") || response.contains("fs_write") || response.contains("execute_bash") || response.contains("use_aws"), "Missing tools"); + println!("โœ… Found core built-in tools"); + + // Verify tool structure elements + assert!(response.contains("name"), "Missing name field"); + assert!(response.contains("description"), "Missing description field"); + assert!(response.contains("input_schema"), "Missing input_schema field"); + assert!(response.contains("properties"), "Missing properties field"); + println!("โœ… Found required tool structure: name, description, input_schema, properties"); + + // Check for optional MCP/GitHub tools if present + if response.contains("download_files_from_github") { + println!("โœ… Found GitHub-related tools"); + } + if response.contains("consolidate_findings_to_csv") { + println!("โœ… Found analysis tools"); + } + if response.contains("gh_issue") { + println!("โœ… Found GitHub issue reporting tool"); + } + + // Verify schema structure for at least one tool + if response.contains("type") { + println!("โœ… Found proper schema type definitions"); + } + + println!("โœ… All tools schema content verified!"); + + drop(chat); + + Ok(()) +}*/ + +#[test] +#[cfg(all(feature = "tools", feature = "sanity"))] +fn test_fs_write_and_fs_read_tools() -> Result<(), Box> { + println!("\n๐Ÿ” Testing `fs_write` and `fs_read` tool ... | Description: Tests the fs_write and fs_read tools by creating a file with specific content and reading it back to verify file I/O operations work correctly"); + + let save_path = "demo.txt"; + let _cleanup = FileCleanup { path: save_path }; + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + // Test fs_write tool by asking to create a file with "Hello World" content + let response = chat.execute_command_with_timeout(&format!("Create a file at {} with content 'Hello World'", save_path),Some(2000))?; + + println!("๐Ÿ“ fs_write response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify tool usage indication + assert!(response.contains("Using tool") && response.contains("fs_write"), "Missing fs_write tool usage indication"); + println!("โœ… Found fs_write tool usage indication"); + + // Verify file path in response + assert!(response.contains("demo.txt"), "Missing expected file path"); + println!("โœ… Found expected file path in response"); + + // Allow the tool execution + let allow_response = chat.execute_command_with_timeout("y",Some(2000))?; + + println!("๐Ÿ“ Allow response: {} bytes", allow_response.len()); + println!("๐Ÿ“ ALLOW RESPONSE:"); + println!("{}", allow_response); + println!("๐Ÿ“ END ALLOW RESPONSE"); + + // Verify content reference + assert!(allow_response.contains("Hello World"), "Missing expected content reference"); + println!("โœ… Found expected content reference"); + + // Verify success indication + assert!(allow_response.contains("Created"), "Missing success indication"); + println!("โœ… Found success indication"); + + // Test fs_read tool by asking to read the created file + let response = chat.execute_command_with_timeout(&format!("Read file {}", save_path),Some(2000))?; + + println!("๐Ÿ“ fs_read response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify tool usage indication + assert!(response.contains("Using tool") && response.contains("fs_read"), "Missing fs_read tool usage indication"); + println!("โœ… Found fs_read tool usage indication"); + + // Verify file path in response + assert!(response.contains("demo.txt"), "Missing expected file path"); + println!("โœ… Found expected file path in response"); + + // Verify content reference + assert!(response.contains("Hello World"), "Missing expected content reference"); + println!("โœ… Found expected content reference"); + + println!("โœ… All fs_write and fs_read tool functionality verified!"); + + drop(chat); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "tools", feature = "sanity"))] +fn test_execute_bash_tool() -> Result<(), Box> { + println!("\n๐Ÿ” Testing `execute_bash` tool ... | Description: Tests the execute_bash tool by running the 'pwd' command and verifying proper command execution and output"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + // Test execute_bash tool by asking to run pwd command + let response = chat.execute_command_with_timeout("Run pwd",Some(2000))?; + + println!("๐Ÿ“ execute_bash response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify tool usage indication + assert!(response.contains("Using tool") && response.contains("execute_bash"), "Missing execute_bash tool usage indication"); + println!("โœ… Found execute_bash tool usage indication"); + + // Verify command in response + assert!(response.contains("pwd"), "Missing expected command"); + println!("โœ… Found pwd command in response"); + + // Verify success indication + assert!(response.contains("current working directory"), "Missing success indication"); + println!("โœ… Found success indication"); + + println!("โœ… All execute_bash functionality verified!"); + + drop(chat); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "tools", feature = "sanity"))] +fn test_report_issue_tool() -> Result<(), Box> { + println!("\n๐Ÿ” Testing `report_issue` tool ... | Description: Tests the report_issue reporting functionality by creating a sample issue and verifying the browser opens GitHub for issue submission"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + // Test report_issue tool by asking to report an issue + let response = chat.execute_command_with_timeout("Report an issue: 'File creation not working properly'",Some(2000))?; + + println!("๐Ÿ“ report_issue response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify tool usage indication + assert!(response.contains("Using tool") && response.contains("gh_issue"), "Missing report_issue tool usage indication"); + println!("โœ… Found report_issue tool usage indication"); + + // 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 report_issue functionality verified!"); + + drop(chat); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "tools", feature = "sanity"))] +fn test_use_aws_tool() -> Result<(), Box> { + println!("\n๐Ÿ” Testing `use_aws` tool ... | Description: Tests the use_aws tool by executing AWS commands to describe EC2 instances and verifying proper AWS CLI integration"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + // Test use_aws tool by asking to describe EC2 instances in us-west-2 + let response = chat.execute_command_with_timeout("Describe EC2 instances in us-west-2",Some(2000))?; + + println!("๐Ÿ“ use_aws response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify tool usage indication + assert!(response.contains("Using tool") && response.contains("use_aws"), "Missing use_aws tool usage indication"); + println!("โœ… Found use_aws tool usage indication"); + + // Verify command executed successfully. + assert!(response.contains("aws"), "Missing aws information"); + println!("โœ… Found aws information"); + + println!("โœ… All use_aws functionality verified!"); + + drop(chat); + + Ok(()) +} + +#[test] +#[cfg(all(feature = "tools", feature = "sanity"))] +fn test_trust_execute_bash_for_direct_execution() -> Result<(), Box> { + println!("\n๐Ÿ” Testing Trust execute_bash for direct execution ... | Description: Tests the ability to trust the execute_bash tool so it runs commands without asking for user confirmation each time"); + + let session = q_chat_helper::get_chat_session(); + let mut chat = session.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + + // First, trust the execute_bash tool + let trust_response = chat.execute_command_with_timeout("/tools trust execute_bash",Some(2000))?; + + println!("๐Ÿ“ Trust response: {} bytes", trust_response.len()); + println!("๐Ÿ“ TRUST OUTPUT:"); + println!("{}", trust_response); + println!("๐Ÿ“ END TRUST OUTPUT"); + + // Verify trust confirmation + assert!(trust_response.contains("trusted") || trust_response.contains("execute_bash"), "Missing trust confirmation"); + println!("โœ… Found trust confirmation"); + + // Now test execute_bash tool with a simple command that should run directly without confirmation + let response = chat.execute_command_with_timeout("Run mkdir -p test_dir && echo 'test' > test_dir/test.txt",Some(2000))?; + + println!("๐Ÿ“ execute_bash response: {} bytes", response.len()); + println!("๐Ÿ“ FULL OUTPUT:"); + println!("{}", response); + println!("๐Ÿ“ END OUTPUT"); + + // Verify tool usage indication + assert!(response.contains("Using tool") && response.contains("execute_bash"), "Missing execute_bash tool usage indication"); + println!("โœ… Found execute_bash tool usage indication"); + + // Verify the command was executed directly without asking for confirmation + assert!(response.contains("Created") && response.contains("directory") && response.contains("test_dir") , "Missing success message"); + println!("โœ… Found success message"); + + println!("โœ… All trusted execute_bash functionality verified!"); + + chat.execute_command_with_timeout("Delete the directory test_dir/test.txt",Some(2000))?; + + println!("โœ… Directory successfully deleted"); + + drop(chat); + + Ok(()) +} \ No newline at end of file