diff --git a/docs/configuration.md b/docs/configuration.md index 95be3b9..db96b5d 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -21,7 +21,7 @@ require("eca").setup({ usage_string_format = "{messageCost} / {sessionCost}", -- === BEHAVIOR === - behavior = { + behaviour = { -- Set keymaps automatically auto_set_keymaps = true, @@ -29,7 +29,7 @@ require("eca").setup({ auto_focus_sidebar = true, -- Start server automatically - auto_start_server = false, + auto_start_server = true, -- Download server automatically if not found auto_download = true, @@ -114,7 +114,7 @@ require("eca").setup({ ### Minimalist ```lua require("eca").setup({ - behavior = { show_status_updates = false }, + behaviour = { show_status_updates = false }, windows = { width = 30 }, chat = { headers = { @@ -128,7 +128,7 @@ require("eca").setup({ ### Visual/UX focused ```lua require("eca").setup({ - behavior = { auto_focus_sidebar = true }, + behaviour = { auto_focus_sidebar = true }, windows = { width = 50, wrap = true, @@ -149,7 +149,7 @@ require("eca").setup({ require("eca").setup({ debug = true, server_args = "--log-level debug", - behavior = { + behaviour = { auto_start_server = true, show_status_updates = true, }, @@ -164,7 +164,7 @@ require("eca").setup({ ### Performance-oriented ```lua require("eca").setup({ - behavior = { + behaviour = { auto_focus_sidebar = false, show_status_updates = false, }, diff --git a/docs/installation.md b/docs/installation.md index 7167653..c5df383 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -53,7 +53,7 @@ Advanced setup example: opts = { debug = false, server_path = "", - behavior = { + behaviour = { auto_set_keymaps = true, auto_focus_sidebar = true, }, diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index d37e5fc..92df5c0 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -36,7 +36,7 @@ Solutions: Symptoms: `ec` doesn't open chat Solutions: -- Ensure `behavior.auto_set_keymaps = true` +- Ensure `behaviour.auto_set_keymaps = true` - Confirm your `` key (default: `\`) - Configure shortcuts manually: @@ -61,5 +61,5 @@ Symptoms: Lag when typing, slow responses Solutions: - Reduce window width: `windows.width = 25` -- Disable visual updates: `behavior.show_status_updates = false` +- Disable visual updates: `behaviour.show_status_updates = false` - Use the minimalist configuration preset diff --git a/lua/eca/api.lua b/lua/eca/api.lua index 851a102..8ebc6f0 100644 --- a/lua/eca/api.lua +++ b/lua/eca/api.lua @@ -11,11 +11,6 @@ local M = {} function M.chat(opts) opts = opts or {} local eca = require("eca") - - if not M.is_server_running() then - M.start_server() - end - eca.open_sidebar(opts) end diff --git a/lua/eca/commands.lua b/lua/eca/commands.lua index 24d26cc..6a245e6 100644 --- a/lua/eca/commands.lua +++ b/lua/eca/commands.lua @@ -343,50 +343,6 @@ function M.setup() desc = "Emergency fix for treesitter issues in ECA chat", }) - vim.api.nvim_create_user_command("EcaChatSelectModel", function() - local eca = require("eca") - - if not eca or not eca.current or not eca.current.sidebar then - Logger.notify("No active ECA sidebar found", vim.log.levels.WARN) - return - end - - local chat = eca.current.sidebar - local models = chat.mediator:models() - - vim.ui.select(models, { - prompt = "Select ECA Chat Model:", - }, function(choice) - if choice then - chat.mediator:update_selected_model(choice) - end - end) - end, { - desc = "Select current ECA Chat model", - }) - - vim.api.nvim_create_user_command("EcaChatSelectBehavior", function() - local eca = require("eca") - - if not eca or not eca.current or not eca.current.sidebar then - Logger.notify("No active ECA sidebar found", vim.log.levels.WARN) - return - end - - local chat = eca.current.sidebar - local behaviors = chat.mediator:behaviors() - - vim.ui.select(behaviors, { - prompt = "Select ECA Chat Behavior:", - }, function(choice) - if choice then - chat.mediator:update_selected_behavior(choice) - end - end) - end, { - desc = "Select current ECA Chat behavior", - }) - Logger.debug("ECA commands registered") end diff --git a/lua/eca/config.lua b/lua/eca/config.lua index a7f8604..139e1ec 100644 --- a/lua/eca/config.lua +++ b/lua/eca/config.lua @@ -20,10 +20,10 @@ M._defaults = { file = "", max_file_size_mb = 10, -- Maximum log file size in MB before warning }, - behavior = { + behaviour = { auto_set_keymaps = true, auto_focus_sidebar = true, - auto_start_server = false, -- Automatically start server on setup + auto_start_server = true, -- Automatically start server on setup auto_download = true, -- Automatically download server if not found show_status_updates = true, -- Show status updates in notifications }, diff --git a/lua/eca/init.lua b/lua/eca/init.lua index 558cf78..5baed4e 100644 --- a/lua/eca/init.lua +++ b/lua/eca/init.lua @@ -32,7 +32,7 @@ function H.keymaps() require("eca.api").focus() end, { noremap = true }) - if Config.behavior and Config.behavior.auto_set_keymaps then + if Config.behaviour.auto_set_keymaps then Utils.safe_keymap_set({ "n", "v" }, Config.mappings.chat, function() require("eca.api").chat() end, { desc = "eca: open chat" }) @@ -245,12 +245,10 @@ function M.setup(opts) M.state = require("eca.state").new() M.server = Server.new() M.mediator = require("eca.mediator").new(M.server, M.state) - - if Config.behavior and Config.behavior.auto_start_server then - vim.defer_fn(function() - M.server:start() - end, 100) -- Small delay to ensure everything is loaded - end + -- Start server automatically in background + vim.defer_fn(function() + M.server:start() + end, 100) -- Small delay to ensure everything is loaded M.did_setup = true end diff --git a/lua/eca/mediator.lua b/lua/eca/mediator.lua index 9298891..7bdc8e6 100644 --- a/lua/eca/mediator.lua +++ b/lua/eca/mediator.lua @@ -26,30 +26,14 @@ function mediator:send(method, params, callback) self.server:send_request(method, params, callback) end -function mediator:behaviors() - return self.state.config.behaviors.list -end - function mediator:selected_behavior() return self.state.config.behaviors.selected end -function mediator:update_selected_behavior(behavior) - self.state:update_selected_behavior(behavior) -end - -function mediator:models() - return self.state.config.models.list -end - function mediator:selected_model() return self.state.config.models.selected end -function mediator:update_selected_model(model) - self.state:update_selected_model(model) -end - function mediator:tokens_session() return self.state.usage.tokens.session end diff --git a/lua/eca/path_finder.lua b/lua/eca/path_finder.lua index 3557f6e..733fa07 100644 --- a/lua/eca/path_finder.lua +++ b/lua/eca/path_finder.lua @@ -1,5 +1,6 @@ local uv = vim.uv or vim.loop local Utils = require("eca.utils") +local Config = require("eca.config") local Logger = require("eca.logger") ---@class eca.PathFinder @@ -99,7 +100,7 @@ function M:_write_version_file(version) file:write(version) file:close() else - Logger.warn("Could not write version file: " .. self._version_file) + Logger.notify("Could not write version file: " .. self._version_file, vim.log.levels.WARN) end end @@ -135,7 +136,7 @@ function M:_download_latest_server(server_path, version) local download_path = self._cache_dir .. "/" .. artifact_name - Logger.debug("Downloading latest ECA server version from: " .. download_url) + Logger.notify("Downloading latest ECA server version from: " .. download_url, vim.log.levels.INFO) -- Ensure cache directory exists vim.fn.mkdir(self._cache_dir, "p") @@ -149,7 +150,8 @@ function M:_download_latest_server(server_path, version) local download_result = os.execute(download_cmd) if download_result ~= 0 then - error("Failed to download ECA server from: " .. download_url) + Logger.notify("Failed to download ECA server from: " .. download_url, vim.log.levels.ERROR) + return false end -- Extract if it's a zip file @@ -159,7 +161,8 @@ function M:_download_latest_server(server_path, version) local extract_result = os.execute(extract_cmd) if extract_result ~= 0 then - error("Failed to extract ECA server") + Logger.notify("Failed to extract ECA server", vim.log.levels.ERROR) + return false end -- Remove the zip file after extraction @@ -167,30 +170,33 @@ function M:_download_latest_server(server_path, version) end -- Make executable (if not Windows) - if not uv.os_uname().sysname:lower():match("windows") then + if not vim.loop.os_uname().sysname:lower():match("windows") then os.execute("chmod +x " .. vim.fn.shellescape(server_path)) end if not Utils.file_exists(server_path) then - error("ECA server binary not found after download and extraction") + Logger.notify("ECA server binary not found after download and extraction", vim.log.levels.ERROR) + return false end -- Write version file self:_write_version_file(version) - Logger.debug("ECA server downloaded successfully") - + Logger.notify("ECA server downloaded successfully", vim.log.levels.INFO) return true end ---@return string -function M:find(custom_path) +function M:find() -- Check for custom server path first + local custom_path = Config.server_path if custom_path and custom_path:gsub("%s+", "") ~= "" then - if not Utils.file_exists(custom_path) then - error("Custom server path does not exist: " .. custom_path) + if Utils.file_exists(custom_path) then + Logger.debug("Using custom server path: " .. custom_path) + return custom_path + else + Logger.notify("Custom server path does not exist: " .. custom_path, vim.log.levels.WARN) end - return custom_path end local server_path = self:_get_extension_server_path() @@ -209,18 +215,13 @@ function M:find(custom_path) -- Download if server doesn't exist or version is outdated if not server_exists or (latest_version and current_version ~= latest_version) then if not latest_version then - Logger.warn("Could not check for latest version, using existing server") + Logger.notify("Could not check for latest version, using existing server", vim.log.levels.WARN) return server_path end - local success - - local ok, err = pcall(function() - success = self:_download_latest_server(server_path, latest_version) - end) - - if not ok or not success then - error((err and tostring(err)) or "Failed to download ECA server") + local success = self:_download_latest_server(server_path, latest_version) + if not success then + error("Failed to download ECA server") end end diff --git a/lua/eca/server.lua b/lua/eca/server.lua index 5ac3a23..babe87e 100644 --- a/lua/eca/server.lua +++ b/lua/eca/server.lua @@ -1,5 +1,6 @@ local Utils = require("eca.utils") local Config = require("eca.config") +local PathFinder = require("eca.path_finder") local Logger = require("eca.logger") ---@class eca.Server @@ -12,9 +13,8 @@ local Logger = require("eca.logger") ---@field on_stop function Callback when the server stops ---Called when a notification is received(message without an ID) ---@field on_notification fun(server: eca.Server, message: table) +---@field private path_finder eca.PathFinder Server path finder ---@field pending_requests {id: fun(err, data)} -- outgoing requests with callbacks ----@field cwd string Current working directory for the server process ----@field workspace_folders {name: string, uri: string}[] Workspace folders to send on initialize local M = {} ---@param opts? table @@ -22,7 +22,7 @@ local M = {} function M.new(opts) opts = vim.tbl_extend("keep", opts or {}, { on_start = function(pid) - require("eca.logger").debug("Started server with pid " .. pid) + require("eca.logger").notify("Started server with pid " .. pid, vim.log.levels.INFO) end, on_initialize = function() require("eca.logger").notify("Server ready to receive messages", vim.log.levels.INFO) @@ -37,13 +37,7 @@ function M.new(opts) require("eca.observer").notify(message) end) end, - cwd = vim.fn.getcwd(), - workspace_folders = { - { - name = vim.fn.fnamemodify(Utils.get_project_root(), ":t"), - uri = "file://" .. Utils.get_project_root(), - }, - }, + path_finder = PathFinder:new(), }) return setmetatable({ @@ -52,12 +46,11 @@ function M.new(opts) on_initialize = opts.on_initialize, on_stop = opts.on_stop, on_notification = opts.on_notification, + path_finder = opts.path_finder, messages = {}, pending_requests = {}, initialized = false, next_id = 0, - cwd = opts.cwd, - workspace_folders = opts.workspace_folders, }, { __index = M }) end @@ -98,81 +91,72 @@ end ---in testing ---@param opts? eca.ServerStartOpts function M:start(opts) - opts = vim.tbl_deep_extend("force", { initialize = true }, opts or {}) + opts = opts or { initialize = true } - local this_file = debug.getinfo(1, "S").source:sub(2) - local proj_root = vim.fn.fnamemodify(this_file, ":p:h:h:h") - local script_path = proj_root .. "/scripts/server_path.lua" - - local nvim_exe = vim.fn.exepath("nvim") + local server_path + local ok, path_finder_error = pcall(function() + server_path = self.path_finder:find() + end) - if not nvim_exe or nvim_exe == "" then - nvim_exe = "nvim" + if not ok or not server_path then + Logger.notify("Could not find or download ECA server" .. tostring(path_finder_error), vim.log.levels.ERROR) + return end - local lua_cmd = string.format("lua ServerPath.run(%s)", Utils.lua_quote(Config.server_path or "")) - - local cmd = { nvim_exe, "--headless", "--noplugin", (opts.clean and " --clean" or ""), "-u", script_path, "-c", lua_cmd } - - vim.system(cmd, { text = true }, function(out) - if out.code ~= 0 then - Logger.notify(out.stderr, vim.log.levels.ERROR) - return - end - - local stdout_lines = Utils.split_lines(out.stdout) - local server_path = stdout_lines[#stdout_lines] - - Logger.debug("Starting ECA server: " .. server_path) + Logger.debug("Starting ECA server: " .. server_path) - local args = { server_path, "server" } - - if Config.server_args and Config.server_args ~= "" then - vim.list_extend(args, vim.split(Config.server_args, " ")) - end - - opts = vim.tbl_deep_extend("keep", opts, { - cmd = args, - text = true, - cwd = self.cwd, - stdin = true, - stdout = on_stdout(self), - stderr = on_stderr, - ---@param output vim.SystemCompleted - on_exit = function(output) - if output.code ~= 0 then - require("eca.logger").notify(string.format("Server exited with status code %d", output.code), vim.log.levels - .ERROR) - end - end, - }) + local args = { server_path, "server" } + if Config.server_args and Config.server_args ~= "" then + vim.list_extend(args, vim.split(Config.server_args, " ")) + end - local started, process_or_err = pcall(vim.system, opts.cmd, { - cwd = opts.cwd, - text = opts.text, - stdin = opts.stdin, - stdout = opts.stdout, - stderr = opts.stderr, - }, opts.on_exit) + opts = vim.tbl_deep_extend("keep", opts, { + cmd = args, + text = true, + cwd = vim.fn.getcwd(), + stdin = true, + stdout = on_stdout(self), + stderr = on_stderr, + ---@param out vim.SystemCompleted + on_exit = function(out) + if out.code ~= 0 then + require("eca.logger").notify(string.format("Server exited with status code %d", out.code), vim.log.levels.ERROR) + end + end, + }) - if not started then - self.process = nil - Logger.notify(vim.inspect(process_or_err), vim.log.levels.ERROR) - return - end + local started, process_or_err = pcall(vim.system, opts.cmd, { + cwd = opts.cwd, + text = opts.text, + stdin = opts.stdin, + stdout = opts.stdout, + stderr = opts.stderr, + }, opts.on_exit) + + if not started then + self.process = nil + Logger.notify(vim.inspect(process_or_err), vim.log.levels.ERROR) + return + end - self.process = process_or_err - if self.on_start then - self.on_start(process_or_err.pid) - end + self.process = process_or_err + if self.on_start then + self.on_start(process_or_err.pid) + end - if opts.initialize then - self:initialize() - end - end) + if opts.initialize then + self:initialize() + end end function M:initialize() + local workspace_folders = { + { + name = vim.fn.fnamemodify(Utils.get_project_root(), ":t"), + uri = "file://" .. Utils.get_project_root(), + }, + } + self:send_request("initialize", { processId = vim.fn.getpid(), clientInfo = { @@ -184,7 +168,7 @@ function M:initialize() chat = true, }, }, - workspaceFolders = vim.deepcopy(self.workspace_folders), + workspaceFolders = workspace_folders, }, function(err, _) if err then Logger.notify("Could not initialize server: " .. err, vim.log.levels.ERROR) @@ -253,6 +237,12 @@ end ---@param params table ---@param callback? function function M:send_request(method, params, callback) + if not self:is_running() then + Logger.error("ECA server is not running") + if callback then + callback("Server not running", nil) + end + end local id = self:get_next_id() local message = { jsonrpc = "2.0", @@ -266,15 +256,6 @@ function M:send_request(method, params, callback) local json = vim.json.encode(message) table.insert(self.messages, { content = json, content_length = #json }) - - if not self:is_running() then - Logger.error("ECA server is not running") - if callback then - callback("Server not running", nil) - end - return - end - local content = string.format("Content-Length: %d\r\n\r\n%s", #json, json) self.process:write(content) end diff --git a/lua/eca/sidebar.lua b/lua/eca/sidebar.lua index 743c3e9..5786c45 100644 --- a/lua/eca/sidebar.lua +++ b/lua/eca/sidebar.lua @@ -1223,7 +1223,7 @@ function M:_update_welcome_content() local cfg_msg = (cfg.message and cfg.message ~= "" and cfg.message) or nil local welcome_message = cfg_msg or (self.mediator and self.mediator:welcome_message() or nil) - local lines = { "Waiting for server to start..." } + local lines = { "Waiting for welcome message from ECA server..." } if welcome_message and welcome_message ~= "" then lines = Utils.split_lines(welcome_message) @@ -1279,8 +1279,6 @@ function M:_send_message(message) requestId = tostring(os.time()), message = message, contexts = contexts or {}, - model = self.mediator:selected_model(), - behavior = self.mediator:selected_behavior(), }, function(err, result) if err then print("err is " .. err) diff --git a/lua/eca/state.lua b/lua/eca/state.lua index 89677bc..2da885c 100644 --- a/lua/eca/state.lua +++ b/lua/eca/state.lua @@ -168,7 +168,7 @@ end function State:_update_usage(usage) self.usage = { tokens = { - limit = (usage.limit and usage.limit.context) or self.usage.tokens.limit, + limit = (usage.limit and usage.limit.output) or self.usage.tokens.limit, session = usage.sessionTokens or self.usage.tokens.session, }, costs = { @@ -198,28 +198,4 @@ function State:_update_tools(tool) end) end -function State:update_selected_model(model) - if not model or type(model) ~= "string" then - return - end - - self.config.models.selected = model - - vim.schedule(function() - require("eca.observer").notify({ type = "state/updated", content = { config = vim.deepcopy(self.config) } }) - end) -end - -function State:update_selected_behavior(behavior) - if not behavior or type(behavior) ~= "string" then - return - end - - self.config.behaviors.selected = behavior - - vim.schedule(function() - require("eca.observer").notify({ type = "state/updated", content = { config = vim.deepcopy(self.config) } }) - end) -end - return State diff --git a/lua/eca/utils.lua b/lua/eca/utils.lua index 42b78bd..7a46ac8 100644 --- a/lua/eca/utils.lua +++ b/lua/eca/utils.lua @@ -118,11 +118,4 @@ function M.constants() return CONSTANTS end ----@param str string ----@return string -function M.lua_quote(str) - -- Escape backslashes and double quotes for Lua string literal - return '"' .. tostring(str):gsub('\\', '\\\\'):gsub('"', '\\"') .. '"' -end - return M diff --git a/plugin-spec.lua b/plugin-spec.lua index 68a4b71..a441d89 100644 --- a/plugin-spec.lua +++ b/plugin-spec.lua @@ -17,7 +17,7 @@ return { file = "", -- Empty string uses default path max_file_size_mb = 10, }, - behavior = { + behaviour = { auto_set_keymaps = true, auto_focus_sidebar = true, }, diff --git a/scripts/server_path.lua b/scripts/server_path.lua deleted file mode 100644 index f7b0570..0000000 --- a/scripts/server_path.lua +++ /dev/null @@ -1,39 +0,0 @@ -local ServerPath = {} - --- Setup if headless -if #vim.api.nvim_list_uis() == 0 then - _G.ServerPath = ServerPath - - -- hijack to make server tests work on CI using --clean mode - local eca_available = pcall(require, "eca") - if not eca_available then - vim.cmd([[let &rtp.=','.getcwd()]]) - vim.cmd('set rtp+=deps/nui.nvim') - vim.cmd('set rtp+=deps/eca-nvim') - end - - vim.o.swapfile = false - vim.o.backup = false - vim.o.writebackup = false - - require("eca").setup({}) -end - -ServerPath.run = function(custom_path) - local path_finder = require("eca.path_finder"):new() - local path - - local ok, err = pcall(function() - path = path_finder:find(custom_path) - end) - - if not ok then - io.stderr:write(tostring(err)) - os.exit(1) - end - - io.stdout:write(tostring(path)) - os.exit(0) -end - -return ServerPath diff --git a/tests/test_select_commands.lua b/tests/test_select_commands.lua deleted file mode 100644 index 23c028d..0000000 --- a/tests/test_select_commands.lua +++ /dev/null @@ -1,175 +0,0 @@ -local MiniTest = require("mini.test") -local eq = MiniTest.expect.equality -local child = MiniTest.new_child_neovim() - -local function setup_test_environment() - -- Setup commands - require('eca.commands').setup() - - -- Initialize everything - _G.Server = require('eca.server').new() - _G.State = require('eca.state').new() - _G.Mediator = require('eca.mediator').new(_G.Server, _G.State) - _G.Sidebar = require('eca.sidebar').new(1, _G.Mediator) - _G.Eca = require('eca') - _G.Eca.current = { sidebar = _G.Sidebar } - - -- Mock vim.ui.select for testing - _G.selected_choice = nil - _G.shown_items = nil - _G.shown_prompt = nil - _G.original_select = vim.ui.select - - _G.mock_select = function(choice) - _G.selected_choice = choice - vim.ui.select = function(items, opts, on_choice) - _G.shown_items = items - _G.shown_prompt = opts.prompt - on_choice(choice) - end - end - - _G.restore_select = function() - vim.ui.select = _G.original_select - end -end - -local T = MiniTest.new_set({ - hooks = { - pre_case = function() - child.restart({ "-u", "scripts/minimal_init.lua" }) - child.lua_func(setup_test_environment) - end, - post_case = function() - child.lua([[_G.restore_select()]]) - end, - post_once = child.stop, - }, -}) - --- Test EcaChatSelectModel command -T["EcaChatSelectModel"] = MiniTest.new_set() - -T["EcaChatSelectModel"]["command is registered"] = function() - local commands = child.lua_get("vim.api.nvim_get_commands({})") - eq(type(commands.EcaChatSelectModel), "table") - eq(commands.EcaChatSelectModel.name, "EcaChatSelectModel") -end - -T["EcaChatSelectModel"]["updates state when model selected"] = function() - -- Setup initial state with models - child.lua([[ - _G.State.config.models.list = { "model1", "model2", "model3" } - _G.State.config.models.selected = "model1" - - -- Mock vim.ui.select to auto-select model2 - _G.mock_select("model2") - ]]) - - -- Execute command - child.cmd("EcaChatSelectModel") - - -- Check that state was updated - eq(child.lua_get("_G.State.config.models.selected"), "model2") -end - -T["EcaChatSelectModel"]["handles nil selection"] = function() - -- Setup initial state - child.lua([[ - _G.State.config.models.list = { "model1", "model2" } - _G.State.config.models.selected = "model1" - - -- Mock vim.ui.select to return nil (user cancelled) - _G.mock_select(nil) - ]]) - - -- Execute command - child.cmd("EcaChatSelectModel") - - -- Check that state was NOT updated (still model1) - eq(child.lua_get("_G.State.config.models.selected"), "model1") -end - -T["EcaChatSelectModel"]["displays all available models"] = function() - -- Setup models list - child.lua([[ - _G.State.config.models.list = { "gpt-4", "gpt-3.5-turbo", "claude-3" } - - -- Mock vim.ui.select to capture the items shown - _G.mock_select(nil) - ]]) - - -- Execute command - child.cmd("EcaChatSelectModel") - - -- Verify all models were shown - local shown_items = child.lua_get("_G.shown_items") - eq(shown_items[1], "gpt-4") - eq(shown_items[2], "gpt-3.5-turbo") - eq(shown_items[3], "claude-3") -end - --- Test EcaChatSelectBehavior command -T["EcaChatSelectBehavior"] = MiniTest.new_set() - -T["EcaChatSelectBehavior"]["command is registered"] = function() - local commands = child.lua_get("vim.api.nvim_get_commands({})") - eq(type(commands.EcaChatSelectBehavior), "table") - eq(commands.EcaChatSelectBehavior.name, "EcaChatSelectBehavior") -end - -T["EcaChatSelectBehavior"]["updates state when behavior selected"] = function() - -- Setup initial state with behaviors - child.lua([[ - _G.State.config.behaviors.list = { "helpful", "creative", "concise" } - _G.State.config.behaviors.selected = "helpful" - - -- Mock vim.ui.select to auto-select creative - _G.mock_select("creative") - ]]) - - -- Execute command - child.cmd("EcaChatSelectBehavior") - - -- Check that state was updated - eq(child.lua_get("_G.State.config.behaviors.selected"), "creative") -end - -T["EcaChatSelectBehavior"]["handles nil selection"] = function() - -- Setup initial state - child.lua([[ - _G.State.config.behaviors.list = { "helpful", "creative" } - _G.State.config.behaviors.selected = "helpful" - - -- Mock vim.ui.select to return nil (user cancelled) - _G.mock_select(nil) - ]]) - - -- Execute command - child.cmd("EcaChatSelectBehavior") - - -- Check that state was NOT updated (still helpful) - eq(child.lua_get("_G.State.config.behaviors.selected"), "helpful") -end - -T["EcaChatSelectBehavior"]["displays all available behaviors"] = function() - -- Setup behaviors list - child.lua([[ - _G.State.config.behaviors.list = { "helpful", "creative", "concise", "technical" } - - -- Mock vim.ui.select to capture the items shown - _G.mock_select(nil) - ]]) - - -- Execute command - child.cmd("EcaChatSelectBehavior") - - -- Verify all behaviors were shown - local shown_items = child.lua_get("_G.shown_items") - eq(shown_items[1], "helpful") - eq(shown_items[2], "creative") - eq(shown_items[3], "concise") - eq(shown_items[4], "technical") -end - -return T diff --git a/tests/test_server_integration.lua b/tests/test_server_integration.lua index 94d3e8d..368438b 100644 --- a/tests/test_server_integration.lua +++ b/tests/test_server_integration.lua @@ -38,45 +38,57 @@ local function sleep(ms) end T["server"] = MiniTest.new_set() - T["server"]["start"] = function() - child.lua("_G.server:start({ clean = true })") - child.lua([[ - _G.server_started = vim.wait(10000, function() - return _G.server and _G.server:is_running() - end, 100) - ]]) - eq(child.lua_get("_G.server_started"), true) - sleep(1000) - eq(child.lua_get("_G.server.initialized"), true) -end - -T["server"]["start without initialize"] = function() - child.lua("_G.server:start({ clean = true, initialize = false })") - child.lua([[ - _G.server_started = vim.wait(10000, function() - return _G.server and _G.server:is_running() - end, 100) - ]]) - eq(child.lua_get("_G.server_started"), true) - sleep(1000) - eq(child.lua_get("_G.server.initialized"), false) + child.lua("_G.server.start({cmd = {'fake'}})") + eq(child.lua_get("_G.server.process"), vim.NIL) + child.lua("_G.server:start()") + eq(child.lua_get("_G.server:is_running()"), true) end -T["server"]["start with inexistent path"] = function() - child.lua([[ - Config = require("eca.config") - Config.setup({ server_path = "non-existing-path" } ) - _G.server:start({ clean = true }) - ]]) - child.lua([[ - _G.server_started = vim.wait(1000, function() - return _G.server and _G.server:is_running() - end, 100) - ]]) - eq(string.find(child.lua_get("_G.notifications[1].message"), "non-existing-path", 1 , true) ~= nil, true) - eq(child.lua_get("_G.server_started"), false) - eq(child.lua_get("_G.server.initialized"), false) +T["server"]["initialize"] = function() + local test_cases = { + { + method = "initialize", + want = { + chatBehaviors = { "agent", "plan" }, + chatDefaultBehavior = "agent", + chatDefaultModel = "anthropic/claude-sonnet-4-20250514", + chatWelcomeMessage = "Welcome to ECA!\n\nType '/' for commands\n\n", + models = { + "anthropic/claude-3-5-haiku-20241022", + "anthropic/claude-opus-4-1-20250805", + "anthropic/claude-opus-4-20250514", + "anthropic/claude-sonnet-4-20250514", + "github-copilot/claude-sonnet-4", + "github-copilot/gpt-4.1", + "github-copilot/gpt-5", + "github-copilot/gpt-5-mini", + "openai/gpt-4.1", + "openai/gpt-5", + "openai/gpt-5-mini", + "openai/gpt-5-nano", + "openai/o3", + "openai/o4-mini", + }, + }, + }, + { method = "not a command", want = vim.NIL }, + } + for i, test_case in ipairs(test_cases) do + if i == 1 then + test_case = test_cases[i + 1] + end + child.lua("_G.method = " .. vim.inspect(test_case.method)) + child.lua("_G.server:start({initialize = false})") + child.lua_func(function() + _G.server:send_request(_G.method, {}, function(err, result) + _G.err = err + _G.result = result + end) + end) + sleep(1500) + eq(child.lua_get("_G.result"), test_case.want) + end end return T diff --git a/tests/test_server_path.lua b/tests/test_server_path.lua deleted file mode 100644 index a285171..0000000 --- a/tests/test_server_path.lua +++ /dev/null @@ -1,70 +0,0 @@ -local MiniTest = require("mini.test") -local eq = MiniTest.expect.equality -local child = MiniTest.new_child_neovim() - -local function setup_test_environment() - Utils = require("eca.utils") - _G.cmd = function(custom_path) - return { - "nvim", - "--headless", - "--noplugin", - "--clean", - "--cmd", - [[lua package.preload["eca.path_finder"] = function() - local M = {} - function M.new() - return setmetatable({}, { __index = M }) - end - function M:find(custom_path) - if custom_path == "error" then - error("custom-server-path-error") - end - return (custom_path ~= "" and custom_path) or "no-custom-server-path" - end - return M - end]], - "-u", - "scripts/server_path.lua", - "-c", - string.format("lua ServerPath.run(%s)", Utils.lua_quote(custom_path or "")), - } - end -end - -local T = MiniTest.new_set({ - hooks = { - pre_case = function() - child.restart({ "-u", "scripts/minimal_init.lua" }) - child.lua_func(setup_test_environment) - end, - post_case = function() - end, - post_once = child.stop, - }, -}) - -T["server_path"] = MiniTest.new_set() - -T["server_path"]["run without custom path should print to stdout"] = function() - child.lua("_G.result = vim.system(_G.cmd(), { text = true }):wait()") - eq(child.lua_get("_G.result.code"), 0) - eq(child.lua_get("_G.result.stdout"), "no-custom-server-path") - eq(child.lua_get("_G.result.stderr"), "") -end - -T["server_path"]["run with custom path should print to stdout"] = function() - child.lua("_G.result = vim.system(_G.cmd('custom-server-path'), { text = true }):wait()") - eq(child.lua_get("_G.result.code"), 0) - eq(child.lua_get("_G.result.stdout"), "custom-server-path") - eq(child.lua_get("_G.result.stderr"), "") -end - -T["server_path"]["run with error should print to stderr"] = function() - child.lua("_G.result = vim.system(_G.cmd('error'), { text = true }):wait()") - eq(child.lua_get("_G.result.code"), 1) - eq(child.lua_get("_G.result.stdout"), "") - eq(string.find(child.lua_get("_G.result.stderr"), "custom-server-path-error", 1 , true) ~= nil, true) -end - -return T diff --git a/tests/test_state.lua b/tests/test_state.lua index 9f89a78..66a6d11 100644 --- a/tests/test_state.lua +++ b/tests/test_state.lua @@ -97,7 +97,7 @@ T["updates via observer notifications"]["updates usage on usage content"] = func method = 'chat/contentReceived', params = { content = { type = 'usage', - limit = { context = 1024 }, + limit = { output = 1024 }, sessionTokens = 256, lastMessageCost = '0.42', sessionCost = '3.14', @@ -184,72 +184,4 @@ T["updates via observer notifications"]["updates tools on tool/serverUpdated"] = eq(#updates >= 1, true) end -T["update selected model and behavior"] = MiniTest.new_set() - -T["update selected model and behavior"]["update_selected_model updates config"] = function() - child.lua([[ - _G.State.config.models.list = { "model1", "model2" } - _G.State.config.models.selected = "model1" - - _G.State:update_selected_model("model2") - ]]) - - eq(child.lua_get("_G.State.config.models.selected"), "model2") -end - -T["update selected model and behavior"]["update_selected_model handles nil"] = function() - child.lua([[ - _G.State.config.models.selected = "model1" - - -- Should not update if nil is passed - _G.State:update_selected_model(nil) - ]]) - - eq(child.lua_get("_G.State.config.models.selected"), "model1") -end - -T["update selected model and behavior"]["update_selected_model handles non-string"] = function() - child.lua([[ - _G.State.config.models.selected = "model1" - - -- Should not update if non-string is passed - _G.State:update_selected_model(123) - ]]) - - eq(child.lua_get("_G.State.config.models.selected"), "model1") -end - -T["update selected model and behavior"]["update_selected_behavior updates config"] = function() - child.lua([[ - _G.State.config.behaviors.list = { "helpful", "creative" } - _G.State.config.behaviors.selected = "helpful" - - _G.State:update_selected_behavior("creative") - ]]) - - eq(child.lua_get("_G.State.config.behaviors.selected"), "creative") -end - -T["update selected model and behavior"]["update_selected_behavior handles nil"] = function() - child.lua([[ - _G.State.config.behaviors.selected = "helpful" - - -- Should not update if nil is passed - _G.State:update_selected_behavior(nil) - ]]) - - eq(child.lua_get("_G.State.config.behaviors.selected"), "helpful") -end - -T["update selected model and behavior"]["update_selected_behavior handles non-string"] = function() - child.lua([[ - _G.State.config.behaviors.selected = "helpful" - - -- Should not update if non-string is passed - _G.State:update_selected_behavior(123) - ]]) - - eq(child.lua_get("_G.State.config.behaviors.selected"), "helpful") -end - return T