summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-09-04 07:53:40 +0300
committerPaul Buetow <paul@buetow.org>2025-09-04 07:53:40 +0300
commit7a98d7eeb87c55ae589e78eaf567be29688baffe (patch)
tree1c6e86b2892243ddc23244c63891b19a410b8b93
parent23ea1749d303c1263e8a3d2393dee95d7914ddf7 (diff)
appconfig: add comprehensive tests; achieve >80% coverage for package
-rw-r--r--UNITTESTSTOADD.md511
-rw-r--r--internal/appconfig/config_test.go167
-rw-r--r--scripts/scan_uncovered.go187
3 files changed, 167 insertions, 698 deletions
diff --git a/UNITTESTSTOADD.md b/UNITTESTSTOADD.md
deleted file mode 100644
index 2ebcd02..0000000
--- a/UNITTESTSTOADD.md
+++ /dev/null
@@ -1,511 +0,0 @@
-# Unit tests to consider adding
-
-This report lists functions that do not appear to be referenced in test files in the same package directory. Recommendations are heuristic.
-
-## main (cmd/hexai-lsp)
-
-- cmd/hexai-lsp/main.go — func main
- - exported: false complexity: 5 has-control: true
- - recommendation: add test (non-trivial logic)
-
-## hexaicli (internal/hexaicli)
-
-- internal/hexaicli/run.go — func Run
- - exported: true complexity: 5 has-control: true
- - recommendation: add test (exported)
-- internal/hexaicli/run.go — func newClientFromConfig
- - exported: false complexity: 6 has-control: true
- - recommendation: add test (non-trivial logic)
-
-## llm (internal/llm)
-
-- internal/llm/copilot.go — method (copilotClient).Chat
- - exported: true complexity: 24 has-control: true
- - recommendation: add test (exported)
-- internal/llm/copilot.go — method (copilotClient).CodeCompletion
- - exported: true complexity: 18 has-control: true
- - recommendation: add test (exported)
-- internal/llm/copilot.go — method (copilotClient).DefaultModel
- - exported: true complexity: 1 has-control: false
- - recommendation: add test (exported)
-- internal/llm/copilot.go — method (copilotClient).Name
- - exported: true complexity: 1 has-control: false
- - recommendation: add test (exported)
-- internal/llm/copilot.go — func decodeCopilotChat
- - exported: false complexity: 3 has-control: true
- - recommendation: add test (non-trivial logic)
-- internal/llm/copilot.go — method (copilotClient).ensureSession
- - exported: false complexity: 19 has-control: true
- - recommendation: add test (non-trivial logic)
-- internal/llm/copilot.go — func handleCopilotNon2xx
- - exported: false complexity: 6 has-control: true
- - recommendation: add test (non-trivial logic)
-- internal/llm/copilot.go — method (copilotClient).headersChat
- - exported: false complexity: 3 has-control: false
- - recommendation: add test (non-trivial logic)
-- internal/llm/copilot.go — method (copilotClient).headersGhost
- - exported: false complexity: 3 has-control: false
- - recommendation: add test (non-trivial logic)
-- internal/llm/copilot.go — func newCopilot
- - exported: false complexity: 3 has-control: true
- - recommendation: add test (non-trivial logic)
-- internal/llm/copilot.go — func parseInt64
- - exported: false complexity: 3 has-control: false
- - recommendation: add test (non-trivial logic)
-- internal/llm/copilot.go — func parseJWTExp
- - exported: false complexity: 8 has-control: true
- - recommendation: add test (non-trivial logic)
-- internal/llm/copilot.go — method (copilotClient).postJSON
- - exported: false complexity: 4 has-control: true
- - recommendation: add test (non-trivial logic)
-- internal/llm/copilot.go — func randHex
- - exported: false complexity: 4 has-control: true
- - recommendation: add test (non-trivial logic)
-- internal/llm/ollama.go — method (ollamaClient).Chat
- - exported: true complexity: 20 has-control: true
- - recommendation: add test (exported)
-- internal/llm/ollama.go — method (ollamaClient).ChatStream
- - exported: true complexity: 18 has-control: true
- - recommendation: add test (exported)
-- internal/llm/ollama.go — method (ollamaClient).DefaultModel
- - exported: true complexity: 1 has-control: false
- - recommendation: add test (exported)
-- internal/llm/ollama.go — method (ollamaClient).Name
- - exported: true complexity: 1 has-control: false
- - recommendation: add test (exported)
-- internal/llm/ollama.go — method (ollamaClient).doJSON
- - exported: false complexity: 4 has-control: true
- - recommendation: add test (non-trivial logic)
-- internal/llm/ollama.go — func handleOllamaNon2xx
- - exported: false complexity: 6 has-control: true
- - recommendation: add test (non-trivial logic)
-- internal/llm/ollama.go — method (ollamaClient).logStart
- - exported: false complexity: 3 has-control: true
- - recommendation: add test (non-trivial logic)
-- internal/llm/ollama.go — func newOllama
- - exported: false complexity: 3 has-control: true
- - recommendation: add test (non-trivial logic)
-- internal/llm/openai.go — method (openAIClient).Chat
- - exported: true complexity: 21 has-control: true
- - recommendation: add test (exported)
-- internal/llm/openai.go — method (openAIClient).ChatStream
- - exported: true complexity: 18 has-control: true
- - recommendation: add test (exported)
-- internal/llm/openai.go — method (openAIClient).DefaultModel
- - exported: true complexity: 1 has-control: false
- - recommendation: add test (exported)
-- internal/llm/openai.go — method (openAIClient).Name
- - exported: true complexity: 1 has-control: false
- - recommendation: add test (exported)
-- internal/llm/openai.go — func decodeOpenAIChat
- - exported: false complexity: 3 has-control: true
- - recommendation: add test (non-trivial logic)
-- internal/llm/openai.go — method (openAIClient).doJSON
- - exported: false complexity: 5 has-control: true
- - recommendation: add test (non-trivial logic)
-- internal/llm/openai.go — method (openAIClient).doJSONWithAccept
- - exported: false complexity: 6 has-control: true
- - recommendation: add test (non-trivial logic)
-- internal/llm/openai.go — method (openAIClient).logStart
- - exported: false complexity: 3 has-control: true
- - recommendation: add test (non-trivial logic)
-- internal/llm/openai.go — method (openAIClient).logf
- - exported: false complexity: 1 has-control: false
- - recommendation: optional (helper/trivial)
-- internal/llm/openai.go — func newOpenAI
- - exported: false complexity: 3 has-control: true
- - recommendation: add test (non-trivial logic)
-- internal/llm/provider.go — func NewFromConfig
- - exported: true complexity: 3 has-control: true
- - recommendation: add test (exported)
-- internal/llm/provider.go — func WithMaxTokens
- - exported: true complexity: 1 has-control: false
- - recommendation: add test (exported)
-- internal/llm/provider.go — func WithModel
- - exported: true complexity: 1 has-control: false
- - recommendation: add test (exported)
-- internal/llm/provider.go — func WithStop
- - exported: true complexity: 1 has-control: false
- - recommendation: add test (exported)
-- internal/llm/provider.go — func WithTemperature
- - exported: true complexity: 1 has-control: false
- - recommendation: add test (exported)
-- internal/llm/util.go — func nilStringErr
- - exported: false complexity: 1 has-control: false
- - recommendation: optional (helper/trivial)
-
-## lsp (internal/lsp)
-
-- internal/lsp/context.go — method (Server).fullFileContext
- - exported: false complexity: 3 has-control: true
- - recommendation: add test (exported receiver)
-- internal/lsp/document.go — method (Server).deleteDocument
- - exported: false complexity: 3 has-control: false
- - recommendation: add test (exported receiver)
-- internal/lsp/document.go — method (Server).getDocument
- - exported: false complexity: 3 has-control: false
- - recommendation: add test (exported receiver)
-- internal/lsp/document.go — func hasAny
- - exported: false complexity: 2 has-control: true
- - recommendation: add test (non-trivial logic)
-- internal/lsp/document.go — method (Server).isDefiningNewFunction
- - exported: false complexity: 10 has-control: true
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers.go — method (Server).compCacheTouchLocked
- - exported: false complexity: 4 has-control: true
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers.go — method (Server).completionCacheGet
- - exported: false complexity: 6 has-control: true
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers.go — method (Server).completionCacheKey
- - exported: false complexity: 13 has-control: true
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers.go — method (Server).completionCachePut
- - exported: false complexity: 6 has-control: true
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers.go — method (Server).fallbackCompletionItems
- - exported: false complexity: 1 has-control: false
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers.go — method (Server).handle
- - exported: false complexity: 2 has-control: true
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers.go — func instructionFromSelection
- - exported: false complexity: 3 has-control: true
- - recommendation: add test (non-trivial logic)
-- internal/lsp/handlers.go — method (Server).isTriggerEvent
- - exported: false complexity: 7 has-control: true
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers.go — method (Server).makeCompletionItems
- - exported: false complexity: 6 has-control: true
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers.go — method (Server).reply
- - exported: false complexity: 2 has-control: false
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers_codeaction.go — method (Server).buildDocumentCodeAction
- - exported: false complexity: 6 has-control: true
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers_codeaction.go — method (Server).buildGoUnitTestCodeAction
- - exported: false complexity: 9 has-control: true
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers_codeaction.go — func deriveGoFuncName
- - exported: false complexity: 7 has-control: true
- - recommendation: add test (non-trivial logic)
-- internal/lsp/handlers_codeaction.go — method (Server).diagnosticsInRange
- - exported: false complexity: 7 has-control: true
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers_codeaction.go — func exportName
- - exported: false complexity: 4 has-control: true
- - recommendation: add test (non-trivial logic)
-- internal/lsp/handlers_codeaction.go — func fileExists
- - exported: false complexity: 2 has-control: true
- - recommendation: add test (non-trivial logic)
-- internal/lsp/handlers_codeaction.go — func findGoFunctionAtLine
- - exported: false complexity: 10 has-control: true
- - recommendation: add test (non-trivial logic)
-- internal/lsp/handlers_codeaction.go — method (Server).generateGoTestFunction
- - exported: false complexity: 4 has-control: true
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers_codeaction.go — func greaterPos
- - exported: false complexity: 2 has-control: true
- - recommendation: add test (non-trivial logic)
-- internal/lsp/handlers_codeaction.go — method (Server).handleCodeAction
- - exported: false complexity: 11 has-control: true
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers_codeaction.go — method (Server).handleCodeActionResolve
- - exported: false complexity: 4 has-control: true
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers_codeaction.go — func lessPos
- - exported: false complexity: 2 has-control: true
- - recommendation: add test (non-trivial logic)
-- internal/lsp/handlers_codeaction.go — method (Server).loadFileText
- - exported: false complexity: 6 has-control: true
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers_codeaction.go — func parseGoPackageName
- - exported: false complexity: 2 has-control: true
- - recommendation: add test (non-trivial logic)
-- internal/lsp/handlers_codeaction.go — func rangesOverlap
- - exported: false complexity: 5 has-control: true
- - recommendation: add test (non-trivial logic)
-- internal/lsp/handlers_codeaction.go — method (Server).resolveGoTest
- - exported: false complexity: 30 has-control: true
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers_completion.go — method (Server).buildCompletionMessages
- - exported: false complexity: 5 has-control: true
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers_completion.go — method (Server).buildDocString
- - exported: false complexity: 1 has-control: false
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers_completion.go — func extractTriggerInfo
- - exported: false complexity: 4 has-control: true
- - recommendation: add test (non-trivial logic)
-- internal/lsp/handlers_completion.go — method (Server).handleCompletion
- - exported: false complexity: 5 has-control: true
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers_completion.go — method (Server).logCompletionContext
- - exported: false complexity: 1 has-control: false
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers_completion.go — func parseManualInvoke
- - exported: false complexity: 4 has-control: true
- - recommendation: add test (non-trivial logic)
-- internal/lsp/handlers_completion.go — method (Server).postProcessCompletion
- - exported: false complexity: 6 has-control: true
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers_completion.go — method (Server).prefixHeuristicAllows
- - exported: false complexity: 11 has-control: true
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers_completion.go — method (Server).shouldSuppressForChatTriggerEOL
- - exported: false complexity: 2 has-control: true
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers_completion.go — method (Server).tryProviderNativeCompletion
- - exported: false complexity: 18 has-control: true
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers_completion.go — method (Server).waitForDebounce
- - exported: false complexity: 3 has-control: true
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers_completion.go — method (Server).waitForThrottle
- - exported: false complexity: 4 has-control: true
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers_document.go — method (Server).applyChatEdits
- - exported: false complexity: 10 has-control: true
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers_document.go — method (Server).buildChatHistory
- - exported: false complexity: 10 has-control: true
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers_document.go — method (Server).clientApplyEdit
- - exported: false complexity: 6 has-control: false
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers_document.go — method (Server).clientShowDocument
- - exported: false complexity: 9 has-control: false
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers_document.go — method (Server).deferShowDocument
- - exported: false complexity: 1 has-control: false
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers_document.go — method (Server).detectAndHandleChat
- - exported: false complexity: 4 has-control: true
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers_document.go — method (Server).docBeforeAfter
- - exported: false complexity: 16 has-control: true
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers_document.go — method (Server).handleDidChange
- - exported: false complexity: 2 has-control: true
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers_document.go — method (Server).handleDidClose
- - exported: false complexity: 2 has-control: true
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers_document.go — method (Server).handleDidOpen
- - exported: false complexity: 2 has-control: true
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers_document.go — method (Server).nextReqID
- - exported: false complexity: 6 has-control: false
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers_document.go — func stripTrailingTrigger
- - exported: false complexity: 6 has-control: true
- - recommendation: add test (non-trivial logic)
-- internal/lsp/handlers_execute.go — method (Server).handleExecuteCommand
- - exported: false complexity: 3 has-control: true
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers_init.go — method (Server).handleExit
- - exported: false complexity: 2 has-control: false
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers_init.go — method (Server).handleInitialize
- - exported: false complexity: 4 has-control: true
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers_init.go — method (Server).handleInitialized
- - exported: false complexity: 1 has-control: false
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers_init.go — method (Server).handleShutdown
- - exported: false complexity: 1 has-control: false
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers_utils.go — func applyIndent
- - exported: false complexity: 4 has-control: true
- - recommendation: add test (non-trivial logic)
-- internal/lsp/handlers_utils.go — func buildPrompts
- - exported: false complexity: 4 has-control: true
- - recommendation: add test (non-trivial logic)
-- internal/lsp/handlers_utils.go — method (Server).collectPromptRemovalEdits
- - exported: false complexity: 5 has-control: true
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers_utils.go — func computeTextEditAndFilter
- - exported: false complexity: 5 has-control: true
- - recommendation: add test (non-trivial logic)
-- internal/lsp/handlers_utils.go — func computeWordStart
- - exported: false complexity: 3 has-control: true
- - recommendation: add test (non-trivial logic)
-- internal/lsp/handlers_utils.go — func extractRangeText
- - exported: false complexity: 13 has-control: true
- - recommendation: add test (non-trivial logic)
-- internal/lsp/handlers_utils.go — func inParamList
- - exported: false complexity: 4 has-control: true
- - recommendation: add test (non-trivial logic)
-- internal/lsp/handlers_utils.go — method (Server).incRecvCounters
- - exported: false complexity: 4 has-control: false
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers_utils.go — method (Server).incSentCounters
- - exported: false complexity: 4 has-control: false
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers_utils.go — func isBareDoubleSemicolon
- - exported: false complexity: 5 has-control: true
- - recommendation: add test (non-trivial logic)
-- internal/lsp/handlers_utils.go — func isIdentBoundary
- - exported: false complexity: 1 has-control: false
- - recommendation: optional (helper/trivial)
-- internal/lsp/handlers_utils.go — func isIdentChar
- - exported: false complexity: 1 has-control: false
- - recommendation: optional (helper/trivial)
-- internal/lsp/handlers_utils.go — func labelForCompletion
- - exported: false complexity: 3 has-control: true
- - recommendation: add test (non-trivial logic)
-- internal/lsp/handlers_utils.go — func leadingIndent
- - exported: false complexity: 4 has-control: true
- - recommendation: add test (non-trivial logic)
-- internal/lsp/handlers_utils.go — func lineHasInlinePrompt
- - exported: false complexity: 2 has-control: true
- - recommendation: add test (non-trivial logic)
-- internal/lsp/handlers_utils.go — method (Server).llmRequestOpts
- - exported: false complexity: 3 has-control: true
- - recommendation: add test (exported receiver)
-- internal/lsp/handlers_utils.go — method (Server).logLLMStats
- - exported: false complexity: 13 has-control: true
- - recommendation: add test (exported receiver)
-- internal/lsp/server.go — method (Server).Run
- - exported: true complexity: 1 has-control: true
- - recommendation: add test (exported)
-- internal/lsp/transport.go — method (Server).readMessage
- - exported: false complexity: 7 has-control: true
- - recommendation: add test (exported receiver)
-- internal/lsp/transport.go — method (Server).writeMessage
- - exported: false complexity: 5 has-control: true
- - recommendation: add test (exported receiver)
-
-## main (scripts)
-
-- scripts/scan_uncovered.go — func isExportedIdent
- - exported: false complexity: 3 has-control: true
- - recommendation: add test (non-trivial logic)
-- scripts/scan_uncovered.go — func main
- - exported: false complexity: 12 has-control: true
- - recommendation: add test (non-trivial logic)
-- scripts/scan_uncovered.go — func recommend
- - exported: false complexity: 4 has-control: true
- - recommendation: add test (non-trivial logic)
-- scripts/scan_uncovered.go — func relPath
- - exported: false complexity: 2 has-control: true
- - recommendation: add test (non-trivial logic)
-- scripts/scan_uncovered.go — func typeString
- - exported: false complexity: 1 has-control: false
- - recommendation: optional (helper/trivial)
-
-## appconfig (internal/appconfig)
-
-- internal/appconfig/config.go — func Load
- - exported: true complexity: 6 has-control: true
- - recommendation: add test (exported)
-- internal/appconfig/config.go — func getConfigPath
- - exported: false complexity: 3 has-control: true
- - recommendation: add test (non-trivial logic)
-- internal/appconfig/config.go — func loadFromEnv
- - exported: false complexity: 27 has-control: true
- - recommendation: add test (non-trivial logic)
-- internal/appconfig/config.go — func loadFromFile
- - exported: false complexity: 7 has-control: true
- - recommendation: add test (non-trivial logic)
-- internal/appconfig/config.go — method (App).mergeBasics
- - exported: false complexity: 11 has-control: true
- - recommendation: add test (exported receiver)
-- internal/appconfig/config.go — method (App).mergeProviderFields
- - exported: false complexity: 9 has-control: true
- - recommendation: add test (exported receiver)
-- internal/appconfig/config.go — method (App).mergeWith
- - exported: false complexity: 2 has-control: false
- - recommendation: add test (exported receiver)
-- internal/appconfig/config.go — func newDefaultConfig
- - exported: false complexity: 2 has-control: false
- - recommendation: optional (helper/trivial)
-
-## hexailsp (internal/hexailsp)
-
-- internal/hexailsp/run.go — func buildClientIfNil
- - exported: false complexity: 7 has-control: true
- - recommendation: add test (non-trivial logic)
-- internal/hexailsp/run.go — func ensureFactory
- - exported: false complexity: 2 has-control: true
- - recommendation: add test (non-trivial logic)
-- internal/hexailsp/run.go — func makeServerOptions
- - exported: false complexity: 1 has-control: false
- - recommendation: optional (helper/trivial)
-- internal/hexailsp/run.go — func normalizeLoggingConfig
- - exported: false complexity: 2 has-control: true
- - recommendation: add test (non-trivial logic)
-
-## logging (internal/logging)
-
-- internal/logging/chatlogger.go — method (ChatLogger).LogStart
- - exported: true complexity: 4 has-control: true
- - recommendation: add test (exported)
-- internal/logging/chatlogger.go — func NewChatLogger
- - exported: true complexity: 1 has-control: false
- - recommendation: add test (exported)
-- internal/logging/logging.go — func Bind
- - exported: true complexity: 1 has-control: false
- - recommendation: add test (exported)
-- internal/logging/logging.go — func Logf
- - exported: true complexity: 3 has-control: true
- - recommendation: add test (exported)
-- internal/logging/logging.go — func PreviewForLog
- - exported: true complexity: 2 has-control: true
- - recommendation: add test (exported)
-- internal/logging/logging.go — func SetLogPreviewLimit
- - exported: true complexity: 1 has-control: false
- - recommendation: add test (exported)
-
-## main (.)
-
-- Magefile.go — func Build
- - exported: true complexity: 3 has-control: false
- - recommendation: add test (exported)
-- Magefile.go — func BuildHexaiCLI
- - exported: true complexity: 1 has-control: false
- - recommendation: add test (exported)
-- Magefile.go — func BuildHexaiLSP
- - exported: true complexity: 1 has-control: false
- - recommendation: add test (exported)
-- Magefile.go — func Cover
- - exported: true complexity: 10 has-control: true
- - recommendation: add test (exported)
-- Magefile.go — func CoverAll
- - exported: true complexity: 10 has-control: true
- - recommendation: add test (exported)
-- Magefile.go — func Dev
- - exported: true complexity: 3 has-control: true
- - recommendation: add test (exported)
-- Magefile.go — func DevInstall
- - exported: true complexity: 2 has-control: true
- - recommendation: add test (exported)
-- Magefile.go — func Install
- - exported: true complexity: 7 has-control: true
- - recommendation: add test (exported)
-- Magefile.go — func Lint
- - exported: true complexity: 1 has-control: false
- - recommendation: add test (exported)
-- Magefile.go — func RunCLI
- - exported: true complexity: 3 has-control: false
- - recommendation: add test (exported)
-- Magefile.go — func Test
- - exported: true complexity: 2 has-control: true
- - recommendation: add test (exported)
-- Magefile.go — func Vet
- - exported: true complexity: 1 has-control: false
- - recommendation: add test (exported)
-- Magefile.go — func totalCoveragePercent
- - exported: false complexity: 8 has-control: true
- - recommendation: add test (non-trivial logic)
-- Magefile.go — func warnIfLowCoverage
- - exported: false complexity: 6 has-control: true
- - recommendation: add test (non-trivial logic)
-
-## main (cmd/hexai)
-
-- cmd/hexai/main.go — func main
- - exported: false complexity: 4 has-control: true
- - recommendation: add test (non-trivial logic)
-
diff --git a/internal/appconfig/config_test.go b/internal/appconfig/config_test.go
new file mode 100644
index 0000000..30898a6
--- /dev/null
+++ b/internal/appconfig/config_test.go
@@ -0,0 +1,167 @@
+package appconfig
+
+import (
+ "encoding/json"
+ "io"
+ "log"
+ "os"
+ "path/filepath"
+ "reflect"
+ "strings"
+ "testing"
+)
+
+func newLogger() *log.Logger { return log.New(io.Discard, "", 0) }
+
+func writeJSON(t *testing.T, path string, v any) {
+ t.Helper()
+ if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
+ t.Fatalf("mkdir: %v", err)
+ }
+ f, err := os.Create(path)
+ if err != nil { t.Fatalf("create: %v", err) }
+ defer f.Close()
+ enc := json.NewEncoder(f)
+ if err := enc.Encode(v); err != nil {
+ t.Fatalf("encode json: %v", err)
+ }
+}
+
+func withEnv(t *testing.T, k, v string) { t.Helper(); old := os.Getenv(k); _ = os.Setenv(k, v); t.Cleanup(func(){ _ = os.Setenv(k, old) }) }
+
+func TestLoad_Defaults_NoLogger(t *testing.T) {
+ cfg := Load(nil)
+ if cfg.MaxTokens == 0 || cfg.ContextMode == "" || cfg.ContextWindowLines == 0 || cfg.MaxContextTokens == 0 {
+ t.Fatalf("expected defaults populated, got %+v", cfg)
+ }
+ if cfg.CodingTemperature == nil { t.Fatalf("expected default CodingTemperature") }
+}
+
+func TestLoad_Defaults_WithLogger_NoFile_NoEnv(t *testing.T) {
+ t.Setenv("XDG_CONFIG_HOME", t.TempDir())
+ logger := newLogger()
+ cfg := Load(logger)
+ def := newDefaultConfig()
+ if cfg.MaxTokens != def.MaxTokens || cfg.ContextMode != def.ContextMode || cfg.ContextWindowLines != def.ContextWindowLines {
+ t.Fatalf("expected defaults; got %+v want %+v", cfg, def)
+ }
+}
+
+func TestLoad_FileMerge_And_EnvOverride(t *testing.T) {
+ dir := t.TempDir()
+ t.Setenv("XDG_CONFIG_HOME", dir)
+ cfgPath := filepath.Join(dir, "hexai", "config.json")
+ temp0 := 0.0
+ fileCfg := App{
+ MaxTokens: 123,
+ ContextMode: "file-on-new-func",
+ ContextWindowLines: 50,
+ MaxContextTokens: 999,
+ LogPreviewLimit: 0,
+ CodingTemperature: &temp0,
+ ManualInvokeMinPrefix: 2,
+ CompletionDebounceMs: 150,
+ CompletionThrottleMs: 300,
+ TriggerCharacters: []string{".", ":"},
+ Provider: "openai",
+ OpenAIBaseURL: "https://api.example",
+ OpenAIModel: "gpt-x",
+ OpenAITemperature: &temp0,
+ OllamaBaseURL: "http://ollama",
+ OllamaModel: "llama",
+ OllamaTemperature: &temp0,
+ CopilotBaseURL: "http://copilot",
+ CopilotModel: "ghost",
+ CopilotTemperature: &temp0,
+ }
+ writeJSON(t, cfgPath, fileCfg)
+
+ // Env overrides take precedence
+ withEnv(t, "HEXAI_MAX_TOKENS", "321")
+ withEnv(t, "HEXAI_CONTEXT_MODE", "always-full")
+ withEnv(t, "HEXAI_CONTEXT_WINDOW_LINES", "77")
+ withEnv(t, "HEXAI_MAX_CONTEXT_TOKENS", "888")
+ withEnv(t, "HEXAI_LOG_PREVIEW_LIMIT", "7")
+ withEnv(t, "HEXAI_CODING_TEMPERATURE", "0.7")
+ withEnv(t, "HEXAI_MANUAL_INVOKE_MIN_PREFIX", "5")
+ withEnv(t, "HEXAI_COMPLETION_DEBOUNCE_MS", "333")
+ withEnv(t, "HEXAI_COMPLETION_THROTTLE_MS", "444")
+ withEnv(t, "HEXAI_TRIGGER_CHARACTERS", "., / ,_")
+ withEnv(t, "HEXAI_PROVIDER", "ollama")
+ withEnv(t, "HEXAI_OPENAI_BASE_URL", "https://override")
+ withEnv(t, "HEXAI_OPENAI_MODEL", "gpt-override")
+ withEnv(t, "HEXAI_OPENAI_TEMPERATURE", "0.4")
+ withEnv(t, "HEXAI_OLLAMA_BASE_URL", "http://ollama-override")
+ withEnv(t, "HEXAI_OLLAMA_MODEL", "mistral")
+ withEnv(t, "HEXAI_OLLAMA_TEMPERATURE", "0.6")
+ withEnv(t, "HEXAI_COPILOT_BASE_URL", "http://copilot-override")
+ withEnv(t, "HEXAI_COPILOT_MODEL", "ghost-override")
+ withEnv(t, "HEXAI_COPILOT_TEMPERATURE", "0.3")
+
+ logger := newLogger()
+ cfg := Load(logger)
+
+ // Check overrides
+ if cfg.MaxTokens != 321 || cfg.ContextMode != "always-full" || cfg.ContextWindowLines != 77 || cfg.MaxContextTokens != 888 {
+ t.Fatalf("env overrides (basic) not applied: %+v", cfg)
+ }
+ if cfg.LogPreviewLimit != 7 || cfg.ManualInvokeMinPrefix != 5 || cfg.CompletionDebounceMs != 333 || cfg.CompletionThrottleMs != 444 {
+ t.Fatalf("env overrides (ints) not applied: %+v", cfg)
+ }
+ if cfg.CodingTemperature == nil || *cfg.CodingTemperature != 0.7 {
+ t.Fatalf("env override (CodingTemperature) not applied: %+v", cfg.CodingTemperature)
+ }
+ if want := []string{".", "/", "_"}; !reflect.DeepEqual(cfg.TriggerCharacters, want) {
+ t.Fatalf("env override (TriggerCharacters), got %v want %v", cfg.TriggerCharacters, want)
+ }
+ if cfg.Provider != "ollama" {
+ t.Fatalf("provider override failed: %q", cfg.Provider)
+ }
+ // Provider-specific
+ if cfg.OpenAIBaseURL != "https://override" || cfg.OpenAIModel != "gpt-override" || cfg.OpenAITemperature == nil || *cfg.OpenAITemperature != 0.4 {
+ t.Fatalf("openai overrides not applied: %+v", cfg)
+ }
+ if cfg.OllamaBaseURL != "http://ollama-override" || cfg.OllamaModel != "mistral" || cfg.OllamaTemperature == nil || *cfg.OllamaTemperature != 0.6 {
+ t.Fatalf("ollama overrides not applied: %+v", cfg)
+ }
+ if cfg.CopilotBaseURL != "http://copilot-override" || cfg.CopilotModel != "ghost-override" || cfg.CopilotTemperature == nil || *cfg.CopilotTemperature != 0.3 {
+ t.Fatalf("copilot overrides not applied: %+v", cfg)
+ }
+
+ // Ensure file values would have applied absent env
+ // Spot-check: reset env and reload
+ for _, k := range []string{
+ "HEXAI_MAX_TOKENS","HEXAI_CONTEXT_MODE","HEXAI_CONTEXT_WINDOW_LINES","HEXAI_MAX_CONTEXT_TOKENS","HEXAI_LOG_PREVIEW_LIMIT","HEXAI_CODING_TEMPERATURE","HEXAI_MANUAL_INVOKE_MIN_PREFIX","HEXAI_COMPLETION_DEBOUNCE_MS","HEXAI_COMPLETION_THROTTLE_MS","HEXAI_TRIGGER_CHARACTERS","HEXAI_PROVIDER","HEXAI_OPENAI_BASE_URL","HEXAI_OPENAI_MODEL","HEXAI_OPENAI_TEMPERATURE","HEXAI_OLLAMA_BASE_URL","HEXAI_OLLAMA_MODEL","HEXAI_OLLAMA_TEMPERATURE","HEXAI_COPILOT_BASE_URL","HEXAI_COPILOT_MODEL","HEXAI_COPILOT_TEMPERATURE",
+ } { t.Setenv(k, "") }
+ cfg2 := Load(logger)
+ if cfg2.MaxTokens != 123 || cfg2.ContextMode != "file-on-new-func" || cfg2.ContextWindowLines != 50 || cfg2.MaxContextTokens != 999 || cfg2.LogPreviewLimit != 0 {
+ t.Fatalf("file merge not applied: %+v", cfg2)
+ }
+ if cfg2.CodingTemperature == nil || *cfg2.CodingTemperature != 0.0 {
+ t.Fatalf("file merge (CodingTemperature) not applied: %+v", cfg2.CodingTemperature)
+ }
+ if cfg2.OpenAIBaseURL != "https://api.example" || cfg2.OpenAIModel != "gpt-x" || cfg2.OpenAITemperature == nil || *cfg2.OpenAITemperature != 0.0 {
+ t.Fatalf("file merge (openai) not applied: %+v", cfg2)
+ }
+}
+
+func TestGetConfigPath_XDG(t *testing.T) {
+ dir := t.TempDir()
+ t.Setenv("XDG_CONFIG_HOME", dir)
+ path, err := getConfigPath()
+ if err != nil { t.Fatalf("getConfigPath: %v", err) }
+ if !strings.HasPrefix(path, filepath.Join(dir, "hexai")) || !strings.HasSuffix(path, "config.json") {
+ t.Fatalf("unexpected path: %s", path)
+ }
+}
+
+func TestLoadFromFile_InvalidJSON(t *testing.T) {
+ dir := t.TempDir()
+ t.Setenv("XDG_CONFIG_HOME", dir)
+ cfgPath := filepath.Join(dir, "hexai", "config.json")
+ if err := os.MkdirAll(filepath.Dir(cfgPath), 0o755); err != nil { t.Fatal(err) }
+ if err := os.WriteFile(cfgPath, []byte("{ invalid"), 0o644); err != nil { t.Fatal(err) }
+ _, err := loadFromFile(cfgPath, newLogger())
+ if err == nil { t.Fatalf("expected error for invalid JSON") }
+}
+
diff --git a/scripts/scan_uncovered.go b/scripts/scan_uncovered.go
deleted file mode 100644
index 9eb22fd..0000000
--- a/scripts/scan_uncovered.go
+++ /dev/null
@@ -1,187 +0,0 @@
-// Command scan_uncovered analyzes Go source to find functions without tests
-// and recommends whether to add tests based on export status and basic complexity.
-package main
-
-import (
- "fmt"
- "go/ast"
- "go/parser"
- "go/token"
- "os"
- "path/filepath"
- "sort"
- "strings"
-)
-
-type FuncInfo struct {
- File string
- Package string
- Name string
- Recv string // receiver type for methods
- Exported bool
- Complexity int // naive: number of statements in body
- HasControl bool // has if/for/switch/select
-}
-
-func main() {
- root := "."
- dirs := map[string][]string{}
- _ = filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
- if err != nil { return nil }
- // Skip vendor, tmp, .git, node_modules, build caches
- base := filepath.Base(path)
- if d.IsDir() {
- if (path != "." && strings.HasPrefix(base, ".")) || base == "vendor" || base == "tmp" || base == "node_modules" || base == ".gocache" || base == ".gomodcache" {
- return filepath.SkipDir
- }
- return nil
- }
- if strings.HasSuffix(path, ".go") && !strings.HasSuffix(path, "_test.go") {
- dir := filepath.Dir(path)
- dirs[dir] = append(dirs[dir], path)
- }
- return nil
- })
-
- fset := token.NewFileSet()
- type missing struct{
- pkg string
- dir string
- items []FuncInfo
- }
- var all []missing
-
- for dir, files := range dirs {
- // parse package name from any file
- pkg := ""
- var funcs []FuncInfo
- for _, file := range files {
- f, err := parser.ParseFile(fset, file, nil, 0)
- if err != nil { continue }
- if pkg == "" { pkg = f.Name.Name }
- for _, d := range f.Decls {
- fd, ok := d.(*ast.FuncDecl)
- if !ok || fd.Name == nil { continue }
- // Skip init/test helpers in non-test files? keep all funcs
- info := FuncInfo{File: file, Package: pkg, Name: fd.Name.Name, Exported: ast.IsExported(fd.Name.Name)}
- if fd.Recv != nil && len(fd.Recv.List) > 0 {
- info.Recv = typeString(fd.Recv.List[0].Type)
- }
- if fd.Body != nil {
- info.Complexity = len(fd.Body.List)
- ast.Inspect(fd.Body, func(n ast.Node) bool {
- switch n.(type) {
- case *ast.IfStmt, *ast.ForStmt, *ast.RangeStmt, *ast.SwitchStmt, *ast.SelectStmt:
- info.HasControl = true
- }
- return true
- })
- }
- funcs = append(funcs, info)
- }
- }
- if len(funcs) == 0 { continue }
- // Gather test identifiers and test names in this dir
- testIdents := make(map[string]struct{})
- testNames := make(map[string]struct{})
- _ = filepath.WalkDir(dir, func(p string, d os.DirEntry, err error) error {
- if err != nil { return nil }
- if d.IsDir() { return nil }
- if !strings.HasSuffix(p, "_test.go") { return nil }
- tf, err := parser.ParseFile(fset, p, nil, 0)
- if err != nil { return nil }
- for _, d := range tf.Decls {
- if fd, ok := d.(*ast.FuncDecl); ok && fd.Name != nil {
- if strings.HasPrefix(fd.Name.Name, "Test") { testNames[fd.Name.Name] = struct{}{} }
- }
- }
- ast.Inspect(tf, func(n ast.Node) bool {
- switch x := n.(type) {
- case *ast.Ident:
- testIdents[x.Name] = struct{}{}
- case *ast.SelectorExpr:
- testIdents[x.Sel.Name] = struct{}{}
- }
- return true
- })
- return nil
- })
-
- // Filter funcs missing coverage signal
- var miss []FuncInfo
- for _, fn := range funcs {
- // Heuristic: if function name appears in tests, assume covered
- if _, ok := testIdents[fn.Name]; ok { continue }
- // Methods: also consider receiver type name for TestType_Method style
- if fn.Recv != "" {
- // e.g., TestType_Method often contains Method name as Ident; already covered by above
- }
- // Skip generated or trivial files? we'll decide in recommendation
- miss = append(miss, fn)
- }
- if len(miss) == 0 { continue }
- sort.Slice(miss, func(i, j int) bool {
- if miss[i].File == miss[j].File {
- return miss[i].Name < miss[j].Name
- }
- return miss[i].File < miss[j].File
- })
- all = append(all, missing{pkg: pkg, dir: dir, items: miss})
- }
-
- // Write markdown report to stdout
- fmt.Println("# Unit tests to consider adding")
- fmt.Println()
- fmt.Println("This report lists functions that do not appear to be referenced in test files in the same package directory. Recommendations are heuristic.")
- fmt.Println()
- for _, m := range all {
- fmt.Printf("## %s (%s)\n\n", m.pkg, m.dir)
- for _, fn := range m.items {
- rec := recommend(fn)
- var sig string
- if fn.Recv != "" { sig = fmt.Sprintf("method (%s).%s", fn.Recv, fn.Name) } else { sig = "func " + fn.Name }
- fmt.Printf("- %s — %s\n", relPath(fn.File), sig)
- fmt.Printf(" - exported: %t complexity: %d has-control: %t\n", fn.Exported, fn.Complexity, fn.HasControl)
- fmt.Printf(" - recommendation: %s\n", rec)
- }
- fmt.Println()
- }
-}
-
-func typeString(t ast.Expr) string {
- switch x := t.(type) {
- case *ast.Ident:
- return x.Name
- case *ast.StarExpr:
- return typeString(x.X)
- case *ast.IndexExpr:
- return typeString(x.X)
- case *ast.IndexListExpr:
- return typeString(x.X)
- case *ast.SelectorExpr:
- return x.Sel.Name
- default:
- return ""
- }
-}
-
-func recommend(fn FuncInfo) string {
- // Strongly recommend for exported functions and methods on exported types
- if fn.Exported { return "add test (exported)" }
- if fn.Recv != "" && isExportedIdent(fn.Recv) { return "add test (exported receiver)" }
- // Recommend for functions with control flow or non-trivial body
- if fn.HasControl || fn.Complexity >= 3 { return "add test (non-trivial logic)" }
- // Otherwise, optional
- return "optional (helper/trivial)"
-}
-
-func isExportedIdent(name string) bool {
- if name == "" { return false }
- r := []rune(name)
- return r[0] >= 'A' && r[0] <= 'Z'
-}
-
-func relPath(p string) string {
- if rp, err := filepath.Rel(".", p); err == nil { return rp }
- return p
-}