diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-16 03:10:55 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-16 03:10:55 +0200 |
| commit | 1fc1611fa99993cab5dc8bf0844183285296e3b2 (patch) | |
| tree | c5c9b8b5abac5b5d4c0d56ed90b0580184cc4383 /cmd/hexai-mcp-server | |
| parent | 12090f25a3677291863dbb80277bdad3eaec0324 (diff) | |
Release v0.24.0v0.24.0
Bring unit test coverage from ~75% to 85.1% project-wide. All internal
packages now exceed 80% coverage. Refactored cmd entrypoints to extract
testable run() functions with injectable seams.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'cmd/hexai-mcp-server')
| -rw-r--r-- | cmd/hexai-mcp-server/main.go | 65 | ||||
| -rw-r--r-- | cmd/hexai-mcp-server/main_test.go | 166 |
2 files changed, 213 insertions, 18 deletions
diff --git a/cmd/hexai-mcp-server/main.go b/cmd/hexai-mcp-server/main.go index 557172f..1f52616 100644 --- a/cmd/hexai-mcp-server/main.go +++ b/cmd/hexai-mcp-server/main.go @@ -4,7 +4,7 @@ package main import ( "flag" "fmt" - "log" + "io" "os" "codeberg.org/snonux/hexai/internal" @@ -12,6 +12,12 @@ import ( "codeberg.org/snonux/hexai/internal/hexaimcp" ) +// Seams for testing: override in tests to avoid launching real MCP server. +var ( + runMCP = hexaimcp.Run + runBackfill = hexaimcp.RunBackfill +) + // printDeprecationWarning outputs a deprecation notice to stderr explaining // that hexai-mcp-server is experimental and not actively maintained. func printDeprecationWarning() { @@ -36,6 +42,17 @@ Use at your own risk. fmt.Fprintln(os.Stderr, warning) } +// mcpOptions holds the parsed command-line flags for the MCP server. +type mcpOptions struct { + logPath string + configPath string + promptsDir string + slashCommandSync bool + slashCommandDir string + syncAll bool + showVersion bool +} + func main() { printDeprecationWarning() @@ -49,33 +66,45 @@ func main() { showVersion := flag.Bool("version", false, "print version and exit") flag.Parse() - if *showVersion { - fmt.Println(internal.Version) - return + opts := mcpOptions{ + logPath: *logPath, + configPath: *configPath, + promptsDir: *promptsDir, + slashCommandSync: *slashCommandSync, + slashCommandDir: *slashCommandDir, + syncAll: *syncAll, + showVersion: *showVersion, + } + if err := run(opts, os.Stdin, os.Stdout, os.Stderr); err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) } +} - // If prompts-dir is specified, set environment variable for RunWithFactory - if *promptsDir != "" { - os.Setenv("HEXAI_MCP_PROMPTS_DIR", *promptsDir) +// run executes the MCP server logic with the given options and I/O streams. +func run(opts mcpOptions, stdin io.Reader, stdout, stderr io.Writer) error { + // Set environment variables for RunWithFactory based on flag values + if opts.promptsDir != "" { + os.Setenv("HEXAI_MCP_PROMPTS_DIR", opts.promptsDir) } - if *slashCommandSync { + if opts.slashCommandSync { os.Setenv("HEXAI_MCP_SLASHCOMMAND_SYNC", "true") } - if *slashCommandDir != "" { - os.Setenv("HEXAI_MCP_SLASHCOMMAND_DIR", *slashCommandDir) + if opts.slashCommandDir != "" { + os.Setenv("HEXAI_MCP_SLASHCOMMAND_DIR", opts.slashCommandDir) } - // Handle backfill operation - if *syncAll { - if err := hexaimcp.RunBackfill(*logPath, *configPath); err != nil { - log.Fatalf("backfill error: %v", err) - } - return + if opts.showVersion { + fmt.Fprintln(stdout, internal.Version) + return nil } - if err := hexaimcp.Run(*logPath, *configPath, os.Stdin, os.Stdout, os.Stderr); err != nil { - log.Fatalf("server error: %v", err) + // Handle backfill operation + if opts.syncAll { + return runBackfill(opts.logPath, opts.configPath) } + + return runMCP(opts.logPath, opts.configPath, stdin, stdout, stderr) } // defaultLogPath returns the default MCP log file path in the state directory. diff --git a/cmd/hexai-mcp-server/main_test.go b/cmd/hexai-mcp-server/main_test.go new file mode 100644 index 0000000..cf48954 --- /dev/null +++ b/cmd/hexai-mcp-server/main_test.go @@ -0,0 +1,166 @@ +package main + +import ( + "bytes" + "errors" + "io" + "os" + "strings" + "testing" + + "codeberg.org/snonux/hexai/internal" +) + +func TestPrintDeprecationWarning(t *testing.T) { + r, w, err := os.Pipe() + if err != nil { + t.Fatalf("failed to create pipe: %v", err) + } + + oldStderr := os.Stderr + os.Stderr = w + defer func() { os.Stderr = oldStderr }() + + printDeprecationWarning() + + if err := w.Close(); err != nil { + t.Fatalf("failed to close pipe writer: %v", err) + } + + b, err := io.ReadAll(r) + if err != nil { + t.Fatalf("failed to read pipe: %v", err) + } + + output := string(b) + for _, want := range []string{"DEPRECATION NOTICE", "EXPERIMENTAL", "NOT ACTIVELY MAINTAINED"} { + if !strings.Contains(output, want) { + t.Errorf("expected %q in output, got %q", want, output) + } + } +} + +func TestDefaultLogPath(t *testing.T) { + path := defaultLogPath() + if path == "" { + t.Fatal("expected non-empty log path") + } + if !strings.HasSuffix(path, "hexai-mcp-server.log") { + t.Errorf("expected path to end with hexai-mcp-server.log, got %q", path) + } +} + +func TestRun_ShowVersion(t *testing.T) { + var stdout bytes.Buffer + opts := mcpOptions{showVersion: true} + if err := run(opts, nil, &stdout, nil); err != nil { + t.Fatalf("run --version: %v", err) + } + got := strings.TrimSpace(stdout.String()) + if got != internal.Version { + t.Fatalf("expected version %q, got %q", internal.Version, got) + } +} + +func TestRun_SetsPromptsDir(t *testing.T) { + t.Setenv("HEXAI_MCP_PROMPTS_DIR", "") + opts := mcpOptions{showVersion: true, promptsDir: "/tmp/test-prompts"} + var stdout bytes.Buffer + if err := run(opts, nil, &stdout, nil); err != nil { + t.Fatalf("run: %v", err) + } + if got := os.Getenv("HEXAI_MCP_PROMPTS_DIR"); got != "/tmp/test-prompts" { + t.Fatalf("expected HEXAI_MCP_PROMPTS_DIR=/tmp/test-prompts, got %q", got) + } +} + +func TestRun_SetsSlashCommandSync(t *testing.T) { + t.Setenv("HEXAI_MCP_SLASHCOMMAND_SYNC", "") + opts := mcpOptions{showVersion: true, slashCommandSync: true} + var stdout bytes.Buffer + if err := run(opts, nil, &stdout, nil); err != nil { + t.Fatalf("run: %v", err) + } + if got := os.Getenv("HEXAI_MCP_SLASHCOMMAND_SYNC"); got != "true" { + t.Fatalf("expected HEXAI_MCP_SLASHCOMMAND_SYNC=true, got %q", got) + } +} + +func TestRun_SetsSlashCommandDir(t *testing.T) { + t.Setenv("HEXAI_MCP_SLASHCOMMAND_DIR", "") + opts := mcpOptions{showVersion: true, slashCommandDir: "/tmp/test-cmds"} + var stdout bytes.Buffer + if err := run(opts, nil, &stdout, nil); err != nil { + t.Fatalf("run: %v", err) + } + if got := os.Getenv("HEXAI_MCP_SLASHCOMMAND_DIR"); got != "/tmp/test-cmds" { + t.Fatalf("expected HEXAI_MCP_SLASHCOMMAND_DIR=/tmp/test-cmds, got %q", got) + } +} + +func TestRun_SyncAll(t *testing.T) { + old := runBackfill + t.Cleanup(func() { runBackfill = old }) + + var gotLog, gotConfig string + runBackfill = func(logPath, configPath string) error { + gotLog = logPath + gotConfig = configPath + return nil + } + + opts := mcpOptions{syncAll: true, logPath: "/tmp/test.log", configPath: "/tmp/cfg.toml"} + if err := run(opts, nil, nil, nil); err != nil { + t.Fatalf("run syncAll: %v", err) + } + if gotLog != "/tmp/test.log" { + t.Fatalf("expected logPath=/tmp/test.log, got %q", gotLog) + } + if gotConfig != "/tmp/cfg.toml" { + t.Fatalf("expected configPath=/tmp/cfg.toml, got %q", gotConfig) + } +} + +func TestRun_SyncAllError(t *testing.T) { + old := runBackfill + t.Cleanup(func() { runBackfill = old }) + + wantErr := errors.New("backfill failed") + runBackfill = func(_, _ string) error { return wantErr } + + opts := mcpOptions{syncAll: true} + if err := run(opts, nil, nil, nil); !errors.Is(err, wantErr) { + t.Fatalf("expected backfill error, got: %v", err) + } +} + +func TestRun_MCPServer(t *testing.T) { + old := runMCP + t.Cleanup(func() { runMCP = old }) + + called := false + runMCP = func(logPath, configPath string, stdin io.Reader, stdout, stderr io.Writer) error { + called = true + return nil + } + + opts := mcpOptions{logPath: "/tmp/mcp.log"} + if err := run(opts, nil, nil, nil); err != nil { + t.Fatalf("run MCP: %v", err) + } + if !called { + t.Fatal("expected runMCP to be called") + } +} + +func TestRun_MCPServerError(t *testing.T) { + old := runMCP + t.Cleanup(func() { runMCP = old }) + + wantErr := errors.New("server failed") + runMCP = func(_, _ string, _ io.Reader, _, _ io.Writer) error { return wantErr } + + if err := run(mcpOptions{}, nil, nil, nil); !errors.Is(err, wantErr) { + t.Fatalf("expected server error, got: %v", err) + } +} |
