diff options
| author | Paul Buetow <paul@buetow.org> | 2025-09-05 21:17:25 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-09-05 21:17:25 +0300 |
| commit | 61137206eb7dd6a3df865591d710923838f59f18 (patch) | |
| tree | 127b4738703547436848e799e91fc87cc19b0c26 | |
| parent | b5bbf0f183a39353be0fb469d6aca1c3e03b78d5 (diff) | |
over 80% coverage now
| -rw-r--r-- | AGENTS.md | 4 | ||||
| -rw-r--r-- | PROJECTSTATUS.md | 19 | ||||
| -rw-r--r-- | README.md | 5 | ||||
| -rw-r--r-- | REPORT.md | 124 | ||||
| -rw-r--r-- | docs/coverage.html | 1697 | ||||
| -rw-r--r-- | docs/coverage.out | 2753 | ||||
| -rw-r--r-- | internal/hexaicli/run_test.go | 5 | ||||
| -rw-r--r-- | internal/lsp/init_and_trigger_tests.go | 52 |
8 files changed, 2921 insertions, 1738 deletions
@@ -3,7 +3,6 @@ ## Project Structure & Module Organization - `README.md`: Project overview and quick context. -- `IDEAS.md`: Working notes, concepts, and rough drafts. - `assets/`: Optimized images and brand assets (place new images here). Existing legacy files: `hexai.png`, `hexai-small.png`. - `src/`: Future implementation code. @@ -12,7 +11,6 @@ ## Build, Test, and Development Commands -- Preview Markdown: `glow README.md` (or your editor’s preview). - Lint Markdown: `markdownlint **/*.md` — checks heading/style rules. - Spellcheck: `codespell` — catches common typos. - Optimize images: `pngquant --quality=70-85 input.png -o assets/input.png`. @@ -20,6 +18,8 @@ ## Coding Style & Naming Conventions +- Aim for at least 80% unit test coverage of all source code. +- Ensure that all unit tests pass before merging any changes. - If possible, construct individual methods so that they can be unit tested. But only if it doesn't add too much boilerplate to the code base. - There should be no source code file larger than 1000 lines. If so, split it up into multiple. - There should be no function larger then 50 lines. If so, refactor or split up into multiple smaller functions. diff --git a/PROJECTSTATUS.md b/PROJECTSTATUS.md index 04ff2ed..b9d815f 100644 --- a/PROJECTSTATUS.md +++ b/PROJECTSTATUS.md @@ -1,19 +1,22 @@ # Ideas -## Code qualoty +## Code quality * [/] TODO's in the code to be addressed * [/] No more than 1000 LOC per source file * [/] No more than 50 LOC per function * [/] Each struct type in his own file * [/] Sufficient unit tests +* [ ] Static code analyzer run and fix issues ## Features ### Improvements +* [ ] Modify the LLM triggers to be more consistenc. E.g. use >>text here> or >text here> instead of semicolons? * [X] Include unit test coverage reports * [ ] Change inline triggers to include > to be more consistent with other triggers +* [ ] Use are more stricter linter for auto-generated code (gofumpt i think is such a linter) ### New features @@ -30,6 +33,7 @@ * [ ] Be able to switch the underlying model via a prompt * [X] Fine tune when Large Language Model (LLM) completions trigger, as it seems that there are some cases where the Large Language Model (LLM) receives a request but Helix isn't suggesting any completions. There seems to be something odd with the in logic. Investigate the TriggerChar logic and make sure it matches Helix's expectations. * [X] Can anything else can be done with LSP? +* [ ] Fish and zsh and bash shell integration for command completion? Have access to the current shell history (N last commands), current directory content, and current directory name. Be able to select code blocks and perform code actions on them @@ -48,4 +52,15 @@ Be able to switch LLMs. * [ ] Useful: https://deepwiki.com/helix-editor/helix/4.3-language-server-protocol` -## Usage notes +Can Claude Code be used for Code Completion? + +> Yes, Claude (including Claude's coding capabilities, sometimes referred to as "Claude Code") can be used for code completion. You can provide Claude with a partial code snippet or function, and it can generate the next lines or complete the function based on context and intent. This works similarly to how other AI code assistants (like GitHub Copilot or ChatGPT) provide code completion. + +However, Claude is typically accessed via chat or API, rather than being directly integrated into IDEs for real-time, inline code completion. Some third-party tools or plugins may bridge this gap, but out of the box, Claude is best used for code completion via conversational prompts. + +**In summary:** +- Claude can perform code completion when prompted. +- It is not natively integrated into IDEs for real-time completion, but can be used via chat or API. +- You can use it to finish functions, suggest code, or generate boilerplate based on your input. + +j @@ -4,6 +4,7 @@ Hexai, the AI addition for your Helix Editor (https://helix-editor.com) .. Other editors should work but weren't tested. +It has got improved capabilities for Go code understanding (for example, create unit tests from function), but other programming language work as well. ## Features * LSP Code auto-completion @@ -12,7 +13,7 @@ Hexai, the AI addition for your Helix Editor (https://helix-editor.com) .. Other * Stand-alone command line tool for LLM interaction * Support for OpenAI, GitHub Copilot, and Ollama -AI coded it under human supervision, and a human developer reviewed the code. +AI coded it under human orchestration and supervision following best practices with manual code reviews. ## Documentation @@ -34,5 +35,7 @@ Note: `mage lint` uses `golangci-lint`. Install via `mage devinstall` if needed. ## Install +Either use the Mage method as mentioned above, or install directly with: + - CLI: `go install codeberg.org/snonux/hexai/cmd/hexai@latest` - LSP: `go install codeberg.org/snonux/hexai/cmd/hexai-lsp@latest` diff --git a/REPORT.md b/REPORT.md deleted file mode 100644 index f416edd..0000000 --- a/REPORT.md +++ /dev/null @@ -1,124 +0,0 @@ -# Unit Test Improvement Report - -This report outlines areas for improvement in the project's unit tests. While the existing tests provide good coverage, the following suggestions aim to enhance their realism, robustness, and maintainability. - -## 1. `internal/hexaicli/run_test.go` - -- **`TestRunChat_StreamAndNonStream`**: The fake client and streamer return very simplistic, hardcoded responses (`"Hi!"`, `"Yo"`). - - **Recommendation**: Enhance the fake client to return more realistic, multi-line, or structured code/text responses. This would better test the output handling and parsing logic. Consider adding cases for empty or malformed LLM responses. - -## 2. `internal/lsp/codeaction_test.go` - -- **`TestBuildRewriteCodeAction_LazyAndResolves`**: The `fakeLLM` returns a simple, hardcoded string (`"REWRITTEN"`). - - **Recommendation**: Test with more complex and realistic code transformations. For example, the fake LLM could return a multi-line code block, a function with a different signature, or even code with syntax errors to test how the client-side handles such responses. - -- **`TestBuildDiagnosticsCodeAction_LazyAndResolves`**: Similar to the rewrite action, the `fakeLLM` returns a simple string (`"FIXED"`). - - **Recommendation**: The fake LLM should return a code snippet that actually addresses the provided diagnostic. This would make the test a more faithful representation of the feature's intended behavior. - -## 3. `internal/lsp/handlers_end_to_end_test.go` - -- **`TestDetectAndHandleChat_InsertsReply`**: The `fakeLLM` returns a single word (`"Hello"`). - - **Recommendation**: A more realistic test would involve a multi-word or multi-line response, which would better test the formatting and insertion logic (e.g., how newlines are handled). - -- **`TestHandleCodeActionResolve_Document`**: The `fakeLLM` returns a simple, hardcoded response. - - **Recommendation**: The fake LLM's response should be a more realistic documentation block for the given function. This would help verify that the documentation generation and insertion logic works as expected with real-world-like data. - -## 4. `internal/lsp/completion_prefix_strip_test.go` - -- **`TestTryLLMCompletion_ManualInvokeAfterWhitespace_Allows`**: The `fakeLLM` returns a very short, non-representative code snippet (`"() *CustData"`). - - **Recommendation**: Use a more complete and realistic code suggestion to test the completion logic, including how it handles longer suggestions and potential formatting. - -## 5. `internal/llm/*_http_test.go` (New Findings) - -- **`TestOpenAI_Chat_Success`** in `openai_http_test.go` and **`TestCopilot_EnsureSession_AndChat_Success`** in `copilot_http_test.go` use `httptest` to mock the backend services, which is great. However, the mocked responses are minimal (e.g., `{"choices":[{"message":{"content":"OK"}}]}`). - - **Recommendation**: Expand these tests to handle more complex and realistic payloads from the LLM providers. This includes multi-choice responses, responses with `finish_reason` other than `stop`, and error objects in the response body. This will make the client code more robust. - -## 6. General Recommendations - -- **Table-Driven Tests**: Some test files contain multiple, repetitive test functions that could be consolidated into table-driven tests. This would improve readability and make it easier to add new test cases. Examples include `internal/lsp/handlers_test.go` and `internal/lsp/completion_prefix_strip_test.go`. - -- **More Realistic Mock Data**: Across the board, the mock data used in tests is often very simplistic. While this is acceptable for basic unit tests, creating a set of more realistic mock responses from the LLM would allow for more robust testing of the parsing, formatting, and error-handling logic. This could include: - - Multi-line code snippets. - - Code with complex syntax. - - Responses containing Markdown formatting. - - Malformed or incomplete JSON/code. - - Empty responses. - -By addressing these points, the test suite will be more robust and provide a higher degree of confidence in the application's behavior when interacting with a real LLM. - ---- - -## Plan and Status (living checklist) - -Legend: [ ] pending · [~] in progress · [x] done/partially done - -1) internal/hexaicli/run_test.go -- [ ] Enhance fake client/streamer responses to multi-line/structured outputs in TestRunChat_StreamAndNonStream. -- [ ] Add cases for empty/malformed LLM responses and ensure graceful handling. - -2) internal/lsp/codeaction_test.go and related e2e tests -- [ ] Make fake LLM rewrite responses multi-line and structural (e.g., signature change) and validate insertion. -- [ ] Make diagnostics-fix responses actually address a provided diagnostic; assert the fix is reflected in text edits. -- [ ] Document-code action: return realistic docblocks (multi-line) and assert formatting/placement. - -3) internal/lsp/handlers_end_to_end_test.go - - [x] Use multi-line replies in TestDetectAndHandleChat_InsertsReply; verify newline formatting and insertion contains both lines. - - [x] Use more realistic documentation blocks in TestHandleCodeActionResolve_Document; verified multi-line insertion. - -4) internal/lsp/completion_prefix_strip_test.go -- [ ] Replace short snippet ("() *CustData") with fuller realistic suggestions; add additional cases to exercise prefix/indent logic with longer outputs. - -5) internal/llm/*_http_test.go -- [x] OpenAI success: basic chat completion via httptest. -- [x] OpenAI stream: SSE delta accumulation in ChatStream. -- [x] Copilot token + chat: ensureSession + /chat/completions success. -- [x] Copilot CodeCompletion: SSE-style stream with multiple choices. - - [x] Expand OpenAI mocked responses: multi-choice, different finish_reason, error objects; assert parsing. - - [x] Expand Copilot mocked responses: multi-choice, error object in body; assert parsing and error propagation. - - [x] Additional negative cases: OpenAI no-choices and empty delta; Copilot chat no-choices; SSE malformed and decode-error paths. - -6) General - - [x] Convert repetitive tests to table-driven style where appropriate (e.g., completion prefix/strip; instruction markers; label/filter; code fences/inline spans; buildPrompts variants; computeTextEditAndFilter variants). -- [ ] Introduce a shared set of realistic mock responses (multi-line code, markdown, malformed json) and reuse across tests. - -## Progress (latest) - -- [x] Coverage gates and CI ergonomics - - Added `mage covercheck` with per-package totals and exceptions. - -- [x] Coverage raised to ≥80%: - - internal/lsp: ~81.2% (new e2e and helper tests) - - internal/llm: ~80.3% (OpenAI/Copilot HTTP + SSE + token + CodeCompletion) - - internal/hexaicli, internal/hexailsp, internal/appconfig, internal/logging all ≥90% - -- [x] Provider realism improvements (partial): - - OpenAI: added ChatStream SSE and success path via httptest. - - Copilot: added ensureSession (token) + chat success and Codex CodeCompletion SSE. - - Next: multi-choice and finish_reason variants; error objects coverage. - -## Status updates (since last run) - -- [~] 1) hexaicli: adjusted tests for environment variability; realism enhancements pending. -- [x] 2) lsp code actions: document-code and diagnostics tests now use multi-line responses in fake LLM to better simulate real outputs. -- [x] Added rewrite/diagnostics realism tests that validate multi-line replacements and exact range preservation. -- [x] 3) lsp e2e chat/document: chat test now uses multi-line reply and validates insertion contains both lines; document resolve uses multi-line docblock. -- [x] 4) lsp completion: manual-invoke test now uses a multi-line realistic function signature with body; still passes and exercises formatting. -- [x] 5) llm providers: added OpenAI success + SSE stream and Copilot token+chat + Codex SSE tests; coverage ≥80%. Expanded with multi-choice and error-body cases. -- [x] 6) General: introduced shared fixtures (internal/testutil) and added table-driven tests for code fences, inline spans, label selection, prefix stripping, instruction markers, buildPrompts, and computeTextEditAndFilter. Documented patterns in docs/testing.md. -- [x] Added table-driven tests for instruction marker extraction and prefix stripping. - -## Next actions (prioritized) - -1. LSP realism -- Implement multi-line rewrite/diagnostics/doc responses from fake LLM, assert proper NewText and ranges. -- Expand chat reply test to multi-line; verify inserted formatting. - -2. Provider payload breadth -- OpenAI: multi-choice responses, finish_reason != stop, error bodies; negative SSE chunks. -- Copilot: multi-choice in chat, error body propagation in non-2xx; expand CodeCompletion SSE variants. - -3. Table-driven refactors -- Convert repetitive cases (prefix stripping, instruction extraction, label selection) to table-driven style to ease adding new scenarios. - -4. Negative/malformed inputs -- Add malformed/missing fields, empty model responses, and malformed SSE to assert robust error handling in clients and LSP handlers. diff --git a/docs/coverage.html b/docs/coverage.html index 4976a0c..df02a90 100644 --- a/docs/coverage.html +++ b/docs/coverage.html @@ -61,45 +61,47 @@ <option value="file2">codeberg.org/snonux/hexai/internal/appconfig/config.go (94.6%)</option> - <option value="file3">codeberg.org/snonux/hexai/internal/hexaicli/run.go (63.8%)</option> + <option value="file3">codeberg.org/snonux/hexai/internal/hexaicli/run.go (91.4%)</option> <option value="file4">codeberg.org/snonux/hexai/internal/hexailsp/run.go (92.5%)</option> - <option value="file5">codeberg.org/snonux/hexai/internal/llm/copilot.go (7.9%)</option> + <option value="file5">codeberg.org/snonux/hexai/internal/llm/copilot.go (81.8%)</option> - <option value="file6">codeberg.org/snonux/hexai/internal/llm/ollama.go (13.9%)</option> + <option value="file6">codeberg.org/snonux/hexai/internal/llm/ollama.go (88.0%)</option> - <option value="file7">codeberg.org/snonux/hexai/internal/llm/openai.go (32.6%)</option> + <option value="file7">codeberg.org/snonux/hexai/internal/llm/openai.go (85.5%)</option> - <option value="file8">codeberg.org/snonux/hexai/internal/llm/provider.go (37.9%)</option> + <option value="file8">codeberg.org/snonux/hexai/internal/llm/provider.go (100.0%)</option> - <option value="file9">codeberg.org/snonux/hexai/internal/llm/util.go (0.0%)</option> + <option value="file9">codeberg.org/snonux/hexai/internal/llm/util.go (100.0%)</option> - <option value="file10">codeberg.org/snonux/hexai/internal/logging/chatlogger.go (14.3%)</option> + <option value="file10">codeberg.org/snonux/hexai/internal/logging/chatlogger.go (100.0%)</option> - <option value="file11">codeberg.org/snonux/hexai/internal/logging/logging.go (90.9%)</option> + <option value="file11">codeberg.org/snonux/hexai/internal/logging/logging.go (100.0%)</option> - <option value="file12">codeberg.org/snonux/hexai/internal/lsp/context.go (71.8%)</option> + <option value="file12">codeberg.org/snonux/hexai/internal/lsp/context.go (74.4%)</option> - <option value="file13">codeberg.org/snonux/hexai/internal/lsp/document.go (56.3%)</option> + <option value="file13">codeberg.org/snonux/hexai/internal/lsp/document.go (90.1%)</option> - <option value="file14">codeberg.org/snonux/hexai/internal/lsp/handlers.go (77.8%)</option> + <option value="file14">codeberg.org/snonux/hexai/internal/lsp/handlers.go (91.3%)</option> - <option value="file15">codeberg.org/snonux/hexai/internal/lsp/handlers_codeaction.go (22.9%)</option> + <option value="file15">codeberg.org/snonux/hexai/internal/lsp/handlers_codeaction.go (81.2%)</option> - <option value="file16">codeberg.org/snonux/hexai/internal/lsp/handlers_completion.go (69.8%)</option> + <option value="file16">codeberg.org/snonux/hexai/internal/lsp/handlers_completion.go (85.1%)</option> - <option value="file17">codeberg.org/snonux/hexai/internal/lsp/handlers_document.go (1.8%)</option> + <option value="file17">codeberg.org/snonux/hexai/internal/lsp/handlers_document.go (88.9%)</option> - <option value="file18">codeberg.org/snonux/hexai/internal/lsp/handlers_execute.go (0.0%)</option> + <option value="file18">codeberg.org/snonux/hexai/internal/lsp/handlers_execute.go (75.0%)</option> - <option value="file19">codeberg.org/snonux/hexai/internal/lsp/handlers_init.go (0.0%)</option> + <option value="file19">codeberg.org/snonux/hexai/internal/lsp/handlers_init.go (55.6%)</option> - <option value="file20">codeberg.org/snonux/hexai/internal/lsp/handlers_utils.go (65.3%)</option> + <option value="file20">codeberg.org/snonux/hexai/internal/lsp/handlers_utils.go (88.1%)</option> <option value="file21">codeberg.org/snonux/hexai/internal/lsp/server.go (68.8%)</option> - <option value="file22">codeberg.org/snonux/hexai/internal/lsp/transport.go (17.1%)</option> + <option value="file22">codeberg.org/snonux/hexai/internal/lsp/transport.go (71.4%)</option> + + <option value="file23">codeberg.org/snonux/hexai/internal/testutil/fixtures.go (60.0%)</option> </select> </div> @@ -230,7 +232,7 @@ type App struct { } // Constructor: defaults for App (kept first among functions) -func newDefaultConfig() App <span class="cov5" title="8">{ +func newDefaultConfig() App <span class="cov5" title="9">{ // Coding-friendly default temperature across providers // Users can override per provider in config.json (including 0.0). t := 0.2 @@ -252,18 +254,18 @@ func newDefaultConfig() App <span class="cov5" title="8">{ // Load reads configuration from a file and merges with defaults. // It respects the XDG Base Directory Specification. -func Load(logger *log.Logger) App <span class="cov4" title="7">{ +func Load(logger *log.Logger) App <span class="cov5" title="8">{ cfg := newDefaultConfig() if logger == nil </span><span class="cov3" title="3">{ return cfg // Return defaults if no logger is provided (e.g. in tests) }</span> - <span class="cov3" title="4">configPath, err := getConfigPath() + <span class="cov4" title="5">configPath, err := getConfigPath() if err != nil </span><span class="cov0" title="0">{ logger.Printf("%v", err) // Even if config path cannot be resolved, still allow env overrides below. - }</span> else<span class="cov3" title="4"> { - if fileCfg, err := loadFromFile(configPath, logger); err == nil && fileCfg != nil </span><span class="cov2" title="2">{ + }</span> else<span class="cov4" title="5"> { + if fileCfg, err := loadFromFile(configPath, logger); err == nil && fileCfg != nil </span><span class="cov3" title="3">{ cfg.mergeWith(fileCfg) }</span> // When the config file is missing or invalid, we keep defaults and still @@ -271,14 +273,14 @@ func Load(logger *log.Logger) App <span class="cov4" title="7">{ } // Environment overrides (take precedence over file) - <span class="cov3" title="4">if envCfg := loadFromEnv(logger); envCfg != nil </span><span class="cov1" title="1">{ + <span class="cov4" title="5">if envCfg := loadFromEnv(logger); envCfg != nil </span><span class="cov1" title="1">{ cfg.mergeWith(envCfg) }</span> - <span class="cov3" title="4">return cfg</span> + <span class="cov4" title="5">return cfg</span> } // Private helpers -func loadFromFile(path string, logger *log.Logger) (*App, error) <span class="cov4" title="5">{ +func loadFromFile(path string, logger *log.Logger) (*App, error) <span class="cov4" title="6">{ f, err := os.Open(path) if err != nil </span><span class="cov2" title="2">{ if !os.IsNotExist(err) && logger != nil </span><span class="cov0" title="0">{ @@ -286,7 +288,7 @@ func loadFromFile(path string, logger *log.Logger) (*App, error) <span class="co }</span> <span class="cov2" title="2">return nil, err</span> } - <span class="cov3" title="3">defer f.Close() + <span class="cov3" title="4">defer f.Close() dec := json.NewDecoder(f) var fileCfg App @@ -296,81 +298,81 @@ func loadFromFile(path string, logger *log.Logger) (*App, error) <span class="co }</span> <span class="cov1" title="1">return nil, err</span> } - <span class="cov2" title="2">return &fileCfg, nil</span> + <span class="cov3" title="3">return &fileCfg, nil</span> } -func (a *App) mergeWith(other *App) <span class="cov3" title="3">{ +func (a *App) mergeWith(other *App) <span class="cov3" title="4">{ a.mergeBasics(other) a.mergeProviderFields(other) }</span> // mergeBasics merges general (non-provider) fields. -func (a *App) mergeBasics(other *App) <span class="cov3" title="3">{ +func (a *App) mergeBasics(other *App) <span class="cov3" title="4">{ if other.MaxTokens > 0 </span><span class="cov3" title="3">{ a.MaxTokens = other.MaxTokens }</span> - <span class="cov3" title="3">if s := strings.TrimSpace(other.ContextMode); s != "" </span><span class="cov3" title="3">{ + <span class="cov3" title="4">if s := strings.TrimSpace(other.ContextMode); s != "" </span><span class="cov3" title="3">{ a.ContextMode = s }</span> - <span class="cov3" title="3">if other.ContextWindowLines > 0 </span><span class="cov3" title="3">{ + <span class="cov3" title="4">if other.ContextWindowLines > 0 </span><span class="cov3" title="3">{ a.ContextWindowLines = other.ContextWindowLines }</span> - <span class="cov3" title="3">if other.MaxContextTokens > 0 </span><span class="cov3" title="3">{ + <span class="cov3" title="4">if other.MaxContextTokens > 0 </span><span class="cov3" title="3">{ a.MaxContextTokens = other.MaxContextTokens }</span> - <span class="cov3" title="3">if other.LogPreviewLimit >= 0 </span><span class="cov3" title="3">{ + <span class="cov3" title="4">if other.LogPreviewLimit >= 0 </span><span class="cov3" title="4">{ a.LogPreviewLimit = other.LogPreviewLimit }</span> - <span class="cov3" title="3">if other.CodingTemperature != nil </span><span class="cov3" title="3">{ // allow explicit 0.0 + <span class="cov3" title="4">if other.CodingTemperature != nil </span><span class="cov3" title="3">{ // allow explicit 0.0 a.CodingTemperature = other.CodingTemperature }</span> - <span class="cov3" title="3">if other.ManualInvokeMinPrefix >= 0 </span><span class="cov3" title="3">{ + <span class="cov3" title="4">if other.ManualInvokeMinPrefix >= 0 </span><span class="cov3" title="4">{ a.ManualInvokeMinPrefix = other.ManualInvokeMinPrefix }</span> - <span class="cov3" title="3">if other.CompletionDebounceMs > 0 </span><span class="cov3" title="3">{ a.CompletionDebounceMs = other.CompletionDebounceMs }</span> - <span class="cov3" title="3">if other.CompletionThrottleMs > 0 </span><span class="cov3" title="3">{ a.CompletionThrottleMs = other.CompletionThrottleMs }</span> - <span class="cov3" title="3">if len(other.TriggerCharacters) > 0 </span><span class="cov3" title="3">{ + <span class="cov3" title="4">if other.CompletionDebounceMs > 0 </span><span class="cov3" title="3">{ a.CompletionDebounceMs = other.CompletionDebounceMs }</span> + <span class="cov3" title="4">if other.CompletionThrottleMs > 0 </span><span class="cov3" title="3">{ a.CompletionThrottleMs = other.CompletionThrottleMs }</span> + <span class="cov3" title="4">if len(other.TriggerCharacters) > 0 </span><span class="cov3" title="3">{ a.TriggerCharacters = slices.Clone(other.TriggerCharacters) }</span> - <span class="cov3" title="3">if s := strings.TrimSpace(other.Provider); s != "" </span><span class="cov3" title="3">{ + <span class="cov3" title="4">if s := strings.TrimSpace(other.Provider); s != "" </span><span class="cov3" title="4">{ a.Provider = s }</span> } // mergeProviderFields merges per-provider configuration. -func (a *App) mergeProviderFields(other *App) <span class="cov3" title="3">{ +func (a *App) mergeProviderFields(other *App) <span class="cov3" title="4">{ if s := strings.TrimSpace(other.OpenAIBaseURL); s != "" </span><span class="cov3" title="3">{ a.OpenAIBaseURL = s }</span> - <span class="cov3" title="3">if s := strings.TrimSpace(other.OpenAIModel); s != "" </span><span class="cov3" title="3">{ + <span class="cov3" title="4">if s := strings.TrimSpace(other.OpenAIModel); s != "" </span><span class="cov3" title="4">{ a.OpenAIModel = s }</span> - <span class="cov3" title="3">if other.OpenAITemperature != nil </span><span class="cov3" title="3">{ // allow explicit 0.0 + <span class="cov3" title="4">if other.OpenAITemperature != nil </span><span class="cov3" title="3">{ // allow explicit 0.0 a.OpenAITemperature = other.OpenAITemperature }</span> - <span class="cov3" title="3">if s := strings.TrimSpace(other.OllamaBaseURL); s != "" </span><span class="cov3" title="3">{ + <span class="cov3" title="4">if s := strings.TrimSpace(other.OllamaBaseURL); s != "" </span><span class="cov3" title="3">{ a.OllamaBaseURL = s }</span> - <span class="cov3" title="3">if s := strings.TrimSpace(other.OllamaModel); s != "" </span><span class="cov3" title="3">{ + <span class="cov3" title="4">if s := strings.TrimSpace(other.OllamaModel); s != "" </span><span class="cov3" title="3">{ a.OllamaModel = s }</span> - <span class="cov3" title="3">if other.OllamaTemperature != nil </span><span class="cov3" title="3">{ // allow explicit 0.0 + <span class="cov3" title="4">if other.OllamaTemperature != nil </span><span class="cov3" title="3">{ // allow explicit 0.0 a.OllamaTemperature = other.OllamaTemperature }</span> - <span class="cov3" title="3">if s := strings.TrimSpace(other.CopilotBaseURL); s != "" </span><span class="cov3" title="3">{ + <span class="cov3" title="4">if s := strings.TrimSpace(other.CopilotBaseURL); s != "" </span><span class="cov3" title="3">{ a.CopilotBaseURL = s }</span> - <span class="cov3" title="3">if s := strings.TrimSpace(other.CopilotModel); s != "" </span><span class="cov3" title="3">{ + <span class="cov3" title="4">if s := strings.TrimSpace(other.CopilotModel); s != "" </span><span class="cov3" title="3">{ a.CopilotModel = s }</span> - <span class="cov3" title="3">if other.CopilotTemperature != nil </span><span class="cov3" title="3">{ // allow explicit 0.0 + <span class="cov3" title="4">if other.CopilotTemperature != nil </span><span class="cov3" title="3">{ // allow explicit 0.0 a.CopilotTemperature = other.CopilotTemperature }</span> } -func getConfigPath() (string, error) <span class="cov4" title="5">{ +func getConfigPath() (string, error) <span class="cov4" title="6">{ var configPath string - if xdgConfigHome := os.Getenv("XDG_CONFIG_HOME"); xdgConfigHome != "" </span><span class="cov3" title="4">{ + if xdgConfigHome := os.Getenv("XDG_CONFIG_HOME"); xdgConfigHome != "" </span><span class="cov4" title="5">{ configPath = filepath.Join(xdgConfigHome, "hexai", "config.json") }</span> else<span class="cov1" title="1"> { home, err := os.UserHomeDir() @@ -379,29 +381,29 @@ func getConfigPath() (string, error) <span class="cov4" title="5">{ }</span> <span class="cov1" title="1">configPath = filepath.Join(home, ".config", "hexai", "config.json")</span> } - <span class="cov4" title="5">return configPath, nil</span> + <span class="cov4" title="6">return configPath, nil</span> } // --- Environment overrides --- // loadFromEnv constructs an App containing only fields set via HEXAI_* env vars. // These values should take precedence over file config when merged. -func loadFromEnv(logger *log.Logger) *App <span class="cov3" title="4">{ +func loadFromEnv(logger *log.Logger) *App <span class="cov4" title="5">{ var out App var any bool // helpers - getenv := func(k string) string </span><span class="cov10" title="80">{ return strings.TrimSpace(os.Getenv(k)) }</span> - <span class="cov3" title="4">parseInt := func(k string) (int, bool) </span><span class="cov7" title="28">{ + getenv := func(k string) string </span><span class="cov10" title="100">{ return strings.TrimSpace(os.Getenv(k)) }</span> + <span class="cov4" title="5">parseInt := func(k string) (int, bool) </span><span class="cov7" title="35">{ v := getenv(k) - if v == "" </span><span class="cov7" title="21">{ return 0, false }</span> + if v == "" </span><span class="cov7" title="28">{ return 0, false }</span> <span class="cov4" title="7">n, err := strconv.Atoi(v) if err != nil </span><span class="cov0" title="0">{ if logger != nil </span><span class="cov0" title="0">{ logger.Printf("invalid %s: %v", k, err) }</span> ; <span class="cov0" title="0">return 0, false</span> } <span class="cov4" title="7">return n, true</span> } - <span class="cov3" title="4">parseFloatPtr := func(k string) (*float64, bool) </span><span class="cov6" title="16">{ + <span class="cov4" title="5">parseFloatPtr := func(k string) (*float64, bool) </span><span class="cov6" title="20">{ v := getenv(k) - if v == "" </span><span class="cov6" title="12">{ return nil, false }</span> + if v == "" </span><span class="cov6" title="16">{ return nil, false }</span> <span class="cov3" title="4">f, err := strconv.ParseFloat(v, 64) if err != nil </span><span class="cov0" title="0">{ if logger != nil </span><span class="cov0" title="0">{ logger.Printf("invalid %s: %v", k, err) }</span> @@ -410,34 +412,34 @@ func loadFromEnv(logger *log.Logger) *App <span class="cov3" title="4">{ <span class="cov3" title="4">return &f, true</span> } - <span class="cov3" title="4">if n, ok := parseInt("HEXAI_MAX_TOKENS"); ok </span><span class="cov1" title="1">{ + <span class="cov4" title="5">if n, ok := parseInt("HEXAI_MAX_TOKENS"); ok </span><span class="cov1" title="1">{ out.MaxTokens = n; any = true }</span> - <span class="cov3" title="4">if s := getenv("HEXAI_CONTEXT_MODE"); s != "" </span><span class="cov1" title="1">{ + <span class="cov4" title="5">if s := getenv("HEXAI_CONTEXT_MODE"); s != "" </span><span class="cov1" title="1">{ out.ContextMode = s; any = true }</span> - <span class="cov3" title="4">if n, ok := parseInt("HEXAI_CONTEXT_WINDOW_LINES"); ok </span><span class="cov1" title="1">{ + <span class="cov4" title="5">if n, ok := parseInt("HEXAI_CONTEXT_WINDOW_LINES"); ok </span><span class="cov1" title="1">{ out.ContextWindowLines = n; any = true }</span> - <span class="cov3" title="4">if n, ok := parseInt("HEXAI_MAX_CONTEXT_TOKENS"); ok </span><span class="cov1" title="1">{ + <span class="cov4" title="5">if n, ok := parseInt("HEXAI_MAX_CONTEXT_TOKENS"); ok </span><span class="cov1" title="1">{ out.MaxContextTokens = n; any = true }</span> - <span class="cov3" title="4">if n, ok := parseInt("HEXAI_LOG_PREVIEW_LIMIT"); ok </span><span class="cov1" title="1">{ + <span class="cov4" title="5">if n, ok := parseInt("HEXAI_LOG_PREVIEW_LIMIT"); ok </span><span class="cov1" title="1">{ out.LogPreviewLimit = n; any = true }</span> - <span class="cov3" title="4">if n, ok := parseInt("HEXAI_MANUAL_INVOKE_MIN_PREFIX"); ok </span><span class="cov1" title="1">{ + <span class="cov4" title="5">if n, ok := parseInt("HEXAI_MANUAL_INVOKE_MIN_PREFIX"); ok </span><span class="cov1" title="1">{ out.ManualInvokeMinPrefix = n; any = true }</span> - <span class="cov3" title="4">if n, ok := parseInt("HEXAI_COMPLETION_DEBOUNCE_MS"); ok </span><span class="cov1" title="1">{ + <span class="cov4" title="5">if n, ok := parseInt("HEXAI_COMPLETION_DEBOUNCE_MS"); ok </span><span class="cov1" title="1">{ out.CompletionDebounceMs = n; any = true }</span> - <span class="cov3" title="4">if n, ok := parseInt("HEXAI_COMPLETION_THROTTLE_MS"); ok </span><span class="cov1" title="1">{ + <span class="cov4" title="5">if n, ok := parseInt("HEXAI_COMPLETION_THROTTLE_MS"); ok </span><span class="cov1" title="1">{ out.CompletionThrottleMs = n; any = true }</span> - <span class="cov3" title="4">if f, ok := parseFloatPtr("HEXAI_CODING_TEMPERATURE"); ok </span><span class="cov1" title="1">{ + <span class="cov4" title="5">if f, ok := parseFloatPtr("HEXAI_CODING_TEMPERATURE"); ok </span><span class="cov1" title="1">{ out.CodingTemperature = f; any = true }</span> - <span class="cov3" title="4">if s := getenv("HEXAI_TRIGGER_CHARACTERS"); s != "" </span><span class="cov1" title="1">{ + <span class="cov4" title="5">if s := getenv("HEXAI_TRIGGER_CHARACTERS"); s != "" </span><span class="cov1" title="1">{ parts := strings.Split(s, ",") out.TriggerCharacters = nil for _, p := range parts </span><span class="cov3" title="3">{ @@ -447,24 +449,24 @@ func loadFromEnv(logger *log.Logger) *App <span class="cov3" title="4">{ } <span class="cov1" title="1">any = true</span> } - <span class="cov3" title="4">if s := getenv("HEXAI_PROVIDER"); s != "" </span><span class="cov1" title="1">{ + <span class="cov4" title="5">if s := getenv("HEXAI_PROVIDER"); s != "" </span><span class="cov1" title="1">{ out.Provider = s; any = true }</span> // Provider-specific - <span class="cov3" title="4">if s := getenv("HEXAI_OPENAI_BASE_URL"); s != "" </span><span class="cov1" title="1">{ out.OpenAIBaseURL = s; any = true }</span> - <span class="cov3" title="4">if s := getenv("HEXAI_OPENAI_MODEL"); s != "" </span><span class="cov1" title="1">{ out.OpenAIModel = s; any = true }</span> - <span class="cov3" title="4">if f, ok := parseFloatPtr("HEXAI_OPENAI_TEMPERATURE"); ok </span><span class="cov1" title="1">{ out.OpenAITemperature = f; any = true }</span> + <span class="cov4" title="5">if s := getenv("HEXAI_OPENAI_BASE_URL"); s != "" </span><span class="cov1" title="1">{ out.OpenAIBaseURL = s; any = true }</span> + <span class="cov4" title="5">if s := getenv("HEXAI_OPENAI_MODEL"); s != "" </span><span class="cov1" title="1">{ out.OpenAIModel = s; any = true }</span> + <span class="cov4" title="5">if f, ok := parseFloatPtr("HEXAI_OPENAI_TEMPERATURE"); ok </span><span class="cov1" title="1">{ out.OpenAITemperature = f; any = true }</span> - <span class="cov3" title="4">if s := getenv("HEXAI_OLLAMA_BASE_URL"); s != "" </span><span class="cov1" title="1">{ out.OllamaBaseURL = s; any = true }</span> - <span class="cov3" title="4">if s := getenv("HEXAI_OLLAMA_MODEL"); s != "" </span><span class="cov1" title="1">{ out.OllamaModel = s; any = true }</span> - <span class="cov3" title="4">if f, ok := parseFloatPtr("HEXAI_OLLAMA_TEMPERATURE"); ok </span><span class="cov1" title="1">{ out.OllamaTemperature = f; any = true }</span> + <span class="cov4" title="5">if s := getenv("HEXAI_OLLAMA_BASE_URL"); s != "" </span><span class="cov1" title="1">{ out.OllamaBaseURL = s; any = true }</span> + <span class="cov4" title="5">if s := getenv("HEXAI_OLLAMA_MODEL"); s != "" </span><span class="cov1" title="1">{ out.OllamaModel = s; any = true }</span> + <span class="cov4" title="5">if f, ok := parseFloatPtr("HEXAI_OLLAMA_TEMPERATURE"); ok </span><span class="cov1" title="1">{ out.OllamaTemperature = f; any = true }</span> - <span class="cov3" title="4">if s := getenv("HEXAI_COPILOT_BASE_URL"); s != "" </span><span class="cov1" title="1">{ out.CopilotBaseURL = s; any = true }</span> - <span class="cov3" title="4">if s := getenv("HEXAI_COPILOT_MODEL"); s != "" </span><span class="cov1" title="1">{ out.CopilotModel = s; any = true }</span> - <span class="cov3" title="4">if f, ok := parseFloatPtr("HEXAI_COPILOT_TEMPERATURE"); ok </span><span class="cov1" title="1">{ out.CopilotTemperature = f; any = true }</span> + <span class="cov4" title="5">if s := getenv("HEXAI_COPILOT_BASE_URL"); s != "" </span><span class="cov1" title="1">{ out.CopilotBaseURL = s; any = true }</span> + <span class="cov4" title="5">if s := getenv("HEXAI_COPILOT_MODEL"); s != "" </span><span class="cov1" title="1">{ out.CopilotModel = s; any = true }</span> + <span class="cov4" title="5">if f, ok := parseFloatPtr("HEXAI_COPILOT_TEMPERATURE"); ok </span><span class="cov1" title="1">{ out.CopilotTemperature = f; any = true }</span> - <span class="cov3" title="4">if !any </span><span class="cov3" title="3">{ + <span class="cov4" title="5">if !any </span><span class="cov3" title="4">{ return nil }</span> <span class="cov1" title="1">return &out</span> @@ -492,12 +494,12 @@ import ( // Run executes the Hexai CLI behavior given arguments and I/O streams. // It assumes flags have already been parsed by the caller. -func Run(ctx context.Context, args []string, stdin io.Reader, stdout, st |
