#include "gtest/gtest.h" #include "lumacs/command_system.hpp" #include "lumacs/editor_core.hpp" #include "lumacs/minibuffer_manager.hpp" // For interactive commands #include #include #include #include // For std::find using namespace lumacs; // Test fixture for CommandSystem tests class CommandSystemTest : public ::testing::Test { protected: EditorCore core; CommandSystem* command_system; // Raw pointer to manager owned by core void SetUp() override { // EditorCore's constructor performs full initialization. // We get references to the managers owned by 'core'. command_system = &core.command_system(); // Ensure initial clean state by clearing all commands registered by init.lua (if any) command_system->clear_commands(); // Clear buffer and reset cursor if (core.buffer().line_count() > 0) { core.buffer().clear(); } core.set_cursor({0,0}); } void TearDown() override { // Nothing specific needed. core will clean up its managers. } }; TEST_F(CommandSystemTest, RegisterAndExecuteBasicCommand) { bool command_executed = false; command_system->register_command("test-command", [&command_executed](CommandContext& ctx) -> CommandResult { (void)ctx; // Mark ctx as unused command_executed = true; return {CommandStatus::Success, "Command executed"}; }, "A simple test command."); CommandResult result = command_system->execute("test-command", {}); ASSERT_EQ(result.status, CommandStatus::Success); ASSERT_EQ(result.message, "Command executed"); ASSERT_TRUE(command_executed); } TEST_F(CommandSystemTest, ExecuteNonExistentCommand) { CommandResult result = command_system->execute("non-existent-command", {}); ASSERT_EQ(result.status, CommandStatus::Failure); ASSERT_EQ(result.message, "Unknown command or alias: non-existent-command"); } TEST_F(CommandSystemTest, RegisterWithAliasAndExecute) { bool command_executed = false; command_system->register_command("aliased-command", [&command_executed](CommandContext& ctx) -> CommandResult { (void)ctx; // Mark ctx as unused command_executed = true; return {CommandStatus::Success, "Aliased command executed"}; }, "A command with an alias.", false, "", {"alias1", "alias2"}); CommandResult result = command_system->execute("alias1", {}); ASSERT_EQ(result.status, CommandStatus::Success); ASSERT_EQ(result.message, "Aliased command executed"); ASSERT_TRUE(command_executed); command_executed = false; // Reset for next execution result = command_system->execute("alias2", {}); ASSERT_EQ(result.status, CommandStatus::Success); ASSERT_EQ(result.message, "Aliased command executed"); ASSERT_TRUE(command_executed); // Should also work with canonical name command_executed = false; result = command_system->execute("aliased-command", {}); ASSERT_EQ(result.status, CommandStatus::Success); ASSERT_EQ(result.message, "Aliased command executed"); ASSERT_TRUE(command_executed); } TEST_F(CommandSystemTest, RegisterCommandOverwrite) { bool command_executed_v1 = false; command_system->register_command("overwrite-cmd", [&command_executed_v1](CommandContext& ctx) -> CommandResult { (void)ctx; command_executed_v1 = true; return {CommandStatus::Success, "V1"}; }); // Overwrite with V2 bool command_executed_v2 = false; command_system->register_command("overwrite-cmd", [&command_executed_v2](CommandContext& ctx) -> CommandResult { (void)ctx; command_executed_v2 = true; return {CommandStatus::Success, "V2"}; }); CommandResult result = command_system->execute("overwrite-cmd", {}); ASSERT_EQ(result.status, CommandStatus::Success); ASSERT_EQ(result.message, "V2"); ASSERT_FALSE(command_executed_v1); // V1 should not have been called ASSERT_TRUE(command_executed_v2); // V2 should have been called } TEST_F(CommandSystemTest, AliasConflictResolution) { // Register a command that will be canonical command_system->register_command("master-cmd", [](CommandContext& ctx) { (void)ctx; return CommandResult{CommandStatus::Success, ""}; }, "Master command doc.", false, "", {"alias-conflict"}); // Try to register another command whose alias conflicts with an existing canonical command // Note: The current implementation logs a warning to cerr, which we don't capture here, // but we verify the state of command registration. command_system->register_command("other-cmd", [](CommandContext& ctx) { (void)ctx; return CommandResult{CommandStatus::Success, ""}; }, "Other command doc.", false, "", {"alias-conflict"}); // "alias-conflict" clashes with "master-cmd" alias std::vector names = command_system->get_command_names(); ASSERT_TRUE(std::find(names.begin(), names.end(), "master-cmd") != names.end()); ASSERT_TRUE(std::find(names.begin(), names.end(), "other-cmd") != names.end()); ASSERT_TRUE(std::find(names.begin(), names.end(), "alias-conflict") != names.end()); } TEST_F(CommandSystemTest, GetCommandNames) { command_system->register_command("cmd-a", [](CommandContext&){ return CommandResult{CommandStatus::Success, ""}; }, "Cmd A"); command_system->register_command("cmd-b", [](CommandContext&){ return CommandResult{CommandStatus::Success, ""}; }, "Cmd B", false, "", {"alias-b"}); std::vector names = command_system->get_command_names(); ASSERT_EQ(names.size(), 3); ASSERT_TRUE(std::find(names.begin(), names.end(), "cmd-a") != names.end()); ASSERT_TRUE(std::find(names.begin(), names.end(), "cmd-b") != names.end()); ASSERT_TRUE(std::find(names.begin(), names.end(), "alias-b") != names.end()); } TEST_F(CommandSystemTest, GetCommandDocString) { command_system->register_command("doc-cmd", [](CommandContext&){ return CommandResult{CommandStatus::Success, ""}; }, "This is documentation."); std::optional doc = command_system->get_command_doc_string("doc-cmd"); ASSERT_TRUE(doc.has_value()); ASSERT_EQ(doc.value(), "This is documentation."); // For alias command_system->register_command("cmd-alias", [](CommandContext&){ return CommandResult{CommandStatus::Success, ""}; }, "Doc for alias", false, "", {"alias-for-doc"}); doc = command_system->get_command_doc_string("alias-for-doc"); ASSERT_TRUE(doc.has_value()); ASSERT_EQ(doc.value(), "Doc for alias"); // Non-existent doc = command_system->get_command_doc_string("non-existent"); ASSERT_FALSE(doc.has_value()); } // Tests for CommandContext argument accessors TEST_F(CommandSystemTest, CommandContextGetStringArg) { // Use the fixture's core.minibuffer_manager() CommandContext ctx(core, core.minibuffer_manager(), {"arg1", "arg2"}); ASSERT_EQ(ctx.get_string_arg(0).value(), "arg1"); ASSERT_EQ(ctx.get_string_arg(1).value(), "arg2"); ASSERT_FALSE(ctx.get_string_arg(2).has_value()); // Out of bounds } TEST_F(CommandSystemTest, CommandContextGetIntArg) { // Use the fixture's core.minibuffer_manager() CommandContext ctx(core, core.minibuffer_manager(), {"123", "-45", "not-a-number", "2147483648"}); // 2^31 for out of int range ASSERT_EQ(ctx.get_int_arg(0).value(), 123); ASSERT_EQ(ctx.get_int_arg(1).value(), -45); ASSERT_FALSE(ctx.get_int_arg(2).has_value()); // Invalid argument // Test for out of range (std::stoull can handle it, but stoi might throw) // std::stoi throws std::out_of_range for 2147483648 ASSERT_FALSE(ctx.get_int_arg(3).has_value()); ASSERT_FALSE(ctx.get_int_arg(4).has_value()); // Out of bounds // Ensure the message set by stoi failure is not propagating or crashing } TEST_F(CommandSystemTest, ExecuteInteractiveBasic) { // Register a simple interactive command that expects one string argument command_system->register_command("interactive-string-cmd", [](CommandContext& ctx) -> CommandResult { auto arg = ctx.get_string_arg(0); if (arg) { return {CommandStatus::Success, "Got string: " + arg.value()}; } return {CommandStatus::Failure, "No argument provided"}; }, "An interactive command.", true, "s"); // 's' for string argument // Execute interactive command - should return PendingInput CommandResult result = command_system->execute_interactive("interactive-string-cmd"); ASSERT_EQ(result.status, CommandStatus::PendingInput); ASSERT_EQ(result.message, "Waiting for user input."); // MinibufferManager should now be active and waiting for input ASSERT_TRUE(core.minibuffer_manager().is_active()); ASSERT_EQ(core.minibuffer_manager().get_prompt(), "String: "); // Simulate input via MinibufferManager core.minibuffer_manager().handle_key_event("t"); core.minibuffer_manager().handle_key_event("e"); core.minibuffer_manager().handle_key_event("s"); core.minibuffer_manager().handle_key_event("t"); core.minibuffer_manager().handle_key_event("Return"); // Submit // After submission, the command should have executed ASSERT_FALSE(core.minibuffer_manager().is_active()); // The result is communicated via side-effects (EditorCore message) since it's async ASSERT_EQ(core.last_message(), "Got string: test"); } TEST_F(CommandSystemTest, ExecuteInteractiveCancel) { command_system->register_command("interactive-cancel-cmd", [](CommandContext& ctx) -> CommandResult { (void)ctx; // Mark ctx as unused return {CommandStatus::Success, "Should not be executed"}; }, "An interactive command that can be cancelled.", true, "s"); CommandResult result = command_system->execute_interactive("interactive-cancel-cmd"); ASSERT_EQ(result.status, CommandStatus::PendingInput); // Simulate cancel via MinibufferManager core.minibuffer_manager().handle_key_event("Escape"); // Cancel ASSERT_FALSE(core.minibuffer_manager().is_active()); // We expect the command NOT to be executed ASSERT_NE(core.last_message(), "Should not be executed"); }