gitFlow/internal/app/app_test.go
dimitar d52e0715f2 feat: phase 2 — CLI commands and configuration resolution
Add the Cobra-based command surface and the flag/env/config-file
resolution layer that all commands share.

Configuration (internal/config):
- Load() resolves settings with the documented precedence flags > env >
  config file > defaults, via viper: GITFLOW_-prefixed env vars with
  dot-to-underscore mapping, plus ~/.gitflow.yaml (or $GITFLOW_CONFIG)
- RegisterFlags/NewFlagSet own the flag definitions so every command and
  the tests share a single source of truth
- Config/Validate/Dump cover dir, interval, format (table/json/compact),
  exclude globs, max depth, worker count, and the AI block (enabled,
  provider, model, api_key_env, base_url); ConfigFile records the loaded
  path; Dump renders the effective config as human-readable YAML with the
  interval as a duration string

App orchestration (internal/app):
- New() validates the configuration at the boundary (fail fast)
- ScanOnce() runs discovery then a concurrent status scan, warning on
  stderr and continuing when discovery is only partially successful
  (e.g. permission-denied subtrees), and bundles everything into a
  ScanResult

CLI (cmd/gitflow):
- root command with scan / config / version subcommands
- scan: resolves config, prompts for the parent directory when stdin is a
  TTY and --dir was not given (per the README), runs a single pass, and
  renders the result — interim plain/JSON output until phase 3 lands the
  presenter package
- config: prints the effective configuration
- version: prints the build version (ldflags-injectable)
- signalContext() wires SIGINT/SIGTERM into a cancellable context for
  graceful shutdown

Testing:
- config: defaults, flag overrides, env overrides, flag-beats-env
  precedence, config file loading (including duration and slice values),
  GITFLOW_CONFIG path override, validation failures, and Dump output
- app: config validation on New, end-to-end ScanOnce over a real temp
  repo, and missing-directory errors

Verified: go build, go vet, go test -race, gofmt clean; manual smoke of
`gitflow version`, `gitflow config`, and `gitflow scan -d <dir>` against a
scratch directory with a dirty repo.
2026-08-02 08:38:55 +02:00

75 lines
1.8 KiB
Go

package app
import (
"context"
"os/exec"
"path/filepath"
"testing"
"gitea.oblak.solutions/dimitar/gitFlow/internal/config"
"gitea.oblak.solutions/dimitar/gitFlow/pkg/status"
)
func initGitRepo(t *testing.T, path string) {
t.Helper()
if _, err := exec.LookPath("git"); err != nil {
t.Skip("git not available")
}
cmd := exec.Command("git", "init", "-q", "-b", "main", filepath.Base(path))
cmd.Dir = filepath.Dir(path)
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("git init: %v\n%s", err, out)
}
}
func TestNewValidatesConfig(t *testing.T) {
bad := &config.Config{Dir: "", Format: "xml", Workers: 0}
if _, err := New(bad); err == nil {
t.Error("New(bad config) succeeded, want error")
}
}
func TestScanOnce(t *testing.T) {
root := t.TempDir()
repo := filepath.Join(root, "repo")
initGitRepo(t, repo)
cfg := &config.Config{Dir: root, Format: "table", Workers: 4}
a, err := New(cfg)
if err != nil {
t.Fatalf("New: %v", err)
}
result, err := a.ScanOnce(context.Background())
if err != nil {
t.Fatalf("ScanOnce: %v", err)
}
if result.ParentDir != root {
t.Errorf("ParentDir = %q, want %q", result.ParentDir, root)
}
if len(result.Repos) != 1 {
t.Fatalf("ScanOnce found %d repos, want 1", len(result.Repos))
}
info := result.Repos[0]
if info.Path != repo {
t.Errorf("Repo path = %q, want %q", info.Path, repo)
}
if info.Status != status.StatusClean {
t.Errorf("Repo status = %v, want clean", info.Status)
}
if result.ScannedAt.IsZero() {
t.Error("ScannedAt is zero")
}
}
func TestScanOnceMissingDir(t *testing.T) {
cfg := &config.Config{Dir: filepath.Join(t.TempDir(), "missing"), Format: "table", Workers: 4}
a, err := New(cfg)
if err != nil {
t.Fatalf("New: %v", err)
}
if _, err := a.ScanOnce(context.Background()); err == nil {
t.Error("ScanOnce(missing dir) succeeded, want error")
}
}