From 7a98d7eeb87c55ae589e78eaf567be29688baffe Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Thu, 4 Sep 2025 07:53:40 +0300 Subject: appconfig: add comprehensive tests; achieve >80% coverage for package --- UNITTESTSTOADD.md | 511 -------------------------------------- internal/appconfig/config_test.go | 167 +++++++++++++ scripts/scan_uncovered.go | 187 -------------- 3 files changed, 167 insertions(+), 698 deletions(-) delete mode 100644 UNITTESTSTOADD.md create mode 100644 internal/appconfig/config_test.go delete mode 100644 scripts/scan_uncovered.go 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 -} -- cgit v1.2.3