package check
import (
"encoding/json"
"io"
"os"
"path/filepath"
"strings"
"testing"
"gitcode.com/gitcode-cli/cli/pkg/cmdutil"
"gitcode.com/gitcode-cli/cli/pkg/iostreams"
"gitcode.com/gitcode-cli/cli/pkg/precommit"
)
func TestNewCmdCheck(t *testing.T) {
f := cmdutil.TestFactory()
cmd := NewCmdCheck(f, nil)
if cmd == nil {
t.Fatal("NewCmdCheck returned nil")
}
if cmd.Use != "check" {
t.Errorf("Use = %q, want check", cmd.Use)
}
}
func TestCheckRunNoConfigSkips(t *testing.T) {
io, _, out, _ := iostreams.Test()
r := &stubRunner{}
opts := &CheckOptions{
IO: io,
GitRoot: func() (string, error) { return t.TempDir(), nil },
Runner: r,
}
if err := checkRun(opts); err != nil {
t.Fatalf("checkRun() error = %v", err)
}
if !strings.Contains(out.String(), "No pre-commit configuration") {
t.Fatalf("output = %q", out.String())
}
}
func TestCheckRunNotReadyReturnsExit1(t *testing.T) {
dir := t.TempDir()
writeConfig(t, dir)
io, _, _, errOut := iostreams.Test()
r := &stubRunner{}
opts := &CheckOptions{
IO: io,
GitRoot: func() (string, error) { return dir, nil },
Runner: r,
}
err := checkRun(opts)
if err == nil {
t.Fatal("expected error for not-ready environment")
}
if got := cmdutil.ExitCode(err); got != cmdutil.ExitError {
t.Fatalf("ExitCode = %d, want %d", got, cmdutil.ExitError)
}
if !strings.Contains(errOut.String(), "Environment not ready") {
t.Fatalf("expected stderr hint, got %q", errOut.String())
}
}
func TestCheckRunReportsMissingPrePushHook(t *testing.T) {
dir := t.TempDir()
writeConfig(t, dir)
writeInstalledHookNamed(t, dir, "pre-commit")
io, _, out, errOut := iostreams.Test()
opts := &CheckOptions{
IO: io,
GitRoot: func() (string, error) { return dir, nil },
Runner: &stubRunner{version: "pre-commit 3.7.0\n"},
NoInstall: true,
}
err := checkRun(opts)
if err == nil {
t.Fatal("expected error when pre-push hook is missing")
}
if !strings.Contains(out.String(), "✓ git pre-commit hook initialized") {
t.Fatalf("expected installed pre-commit status, got %q", out.String())
}
if !strings.Contains(out.String(), "✗ git pre-push hook initialized") {
t.Fatalf("expected missing pre-push status, got %q", out.String())
}
if !strings.Contains(errOut.String(), "--hook-type pre-commit --hook-type pre-push") {
t.Fatalf("expected complete install guidance, got %q", errOut.String())
}
}
func TestCheckRunJSONPreservesLegacyHookField(t *testing.T) {
dir := t.TempDir()
writeConfig(t, dir)
writeInstalledHookNamed(t, dir, "pre-commit")
io, _, out, _ := iostreams.Test()
opts := &CheckOptions{
IO: io,
GitRoot: func() (string, error) { return dir, nil },
Runner: &stubRunner{version: "pre-commit 3.7.0\n"},
NoInstall: true,
JSON: true,
}
if err := checkRun(opts); err == nil {
t.Fatal("expected error when pre-push hook is missing")
}
var got precommit.Result
if err := json.Unmarshal(out.Bytes(), &got); err != nil {
t.Fatalf("output is not valid JSON: %v; raw=%q", err, out.String())
}
if !got.HookInstalled || !got.PreCommitHookInstalled {
t.Fatalf("legacy hook_installed should preserve pre-commit state, got %+v", got)
}
if got.HooksInstalled || got.PrePushHookInstalled || got.OK {
t.Fatalf("aggregate readiness should require pre-push, got %+v", got)
}
}
func TestCheckRunJSON(t *testing.T) {
dir := t.TempDir()
writeConfig(t, dir)
writeInstalledHook(t, dir)
io, _, out, _ := iostreams.Test()
r := &stubRunner{version: "pre-commit 3.7.0\n"}
opts := &CheckOptions{
IO: io,
GitRoot: func() (string, error) { return dir, nil },
Runner: r,
JSON: true,
}
if err := checkRun(opts); err != nil {
t.Fatalf("checkRun() error = %v", err)
}
var got precommit.Result
if err := json.Unmarshal(out.Bytes(), &got); err != nil {
t.Fatalf("output is not valid JSON: %v; raw=%q", err, out.String())
}
if !got.OK || !got.ConfigFound || !got.ToolInstalled || !got.HookInstalled || !got.HooksInstalled ||
!got.PreCommitHookInstalled || !got.PrePushHookInstalled {
t.Fatalf("unexpected result: %+v", got)
}
}
func TestCheckRunNotInGitRepo(t *testing.T) {
io, _, _, _ := iostreams.Test()
opts := &CheckOptions{
IO: io,
GitRoot: func() (string, error) { return "", os.ErrNotExist },
Runner: &stubRunner{},
}
err := checkRun(opts)
if err == nil {
t.Fatal("expected error when not in a git repo")
}
}
func TestCheckRunNotInGitRepoJSON(t *testing.T) {
io, _, out, _ := iostreams.Test()
opts := &CheckOptions{
IO: io,
GitRoot: func() (string, error) { return "", os.ErrNotExist },
Runner: &stubRunner{},
JSON: true,
}
err := checkRun(opts)
if err == nil {
t.Fatal("expected error when not in a git repo")
}
var got precommit.Result
if jerr := json.Unmarshal(out.Bytes(), &got); jerr != nil {
t.Fatalf("output is not valid JSON: %v; raw=%q", jerr, out.String())
}
if got.OK || got.Reason != precommit.ReasonNotInRepo {
t.Fatalf("want ok=false reason=%q, got %+v", precommit.ReasonNotInRepo, got)
}
}
func TestCheckRunFlagFailureExits1(t *testing.T) {
dir := t.TempDir()
writeConfig(t, dir)
writeInstalledHook(t, dir)
io, _, _, errOut := iostreams.Test()
r := &stubRunner{version: "pre-commit 3.7.0\n", runErr: true}
opts := &CheckOptions{
IO: io,
GitRoot: func() (string, error) { return dir, nil },
Runner: r,
Run: true,
}
err := checkRun(opts)
if err == nil {
t.Fatal("expected error when pre-commit run fails")
}
if got := cmdutil.ExitCode(err); got != cmdutil.ExitError {
t.Fatalf("ExitCode = %d, want %d", got, cmdutil.ExitError)
}
stderr := errOut.String()
if !strings.Contains(stderr, "pre-commit checks failed") {
t.Fatalf("expected 'pre-commit checks failed', got %q", stderr)
}
if strings.Contains(stderr, "Environment not ready") {
t.Fatalf("must not report 'Environment not ready' on a hook failure: %q", stderr)
}
if !strings.Contains(stderr, "hook failed") {
t.Fatalf("expected captured run output in stderr, got %q", stderr)
}
}
func TestCheckRunFlagFailureJSON(t *testing.T) {
dir := t.TempDir()
writeConfig(t, dir)
writeInstalledHook(t, dir)
io, _, out, _ := iostreams.Test()
r := &stubRunner{version: "pre-commit 3.7.0\n", runErr: true}
opts := &CheckOptions{
IO: io,
GitRoot: func() (string, error) { return dir, nil },
Runner: r,
Run: true,
JSON: true,
}
err := checkRun(opts)
if err == nil {
t.Fatal("expected error when pre-commit run fails")
}
var got precommit.Result
if jerr := json.Unmarshal(out.Bytes(), &got); jerr != nil {
t.Fatalf("output is not valid JSON: %v; raw=%q", jerr, out.String())
}
if got.OK || got.RunResult != "failed" {
t.Fatalf("expected ok=false, run_result=failed, got %+v", got)
}
if got.RunOutput == "" {
t.Fatalf("expected run_output to carry failure detail, got %+v", got)
}
}
func TestCheckRunInstallFailureJSON(t *testing.T) {
dir := t.TempDir()
writeConfig(t, dir)
io, _, out, _ := iostreams.Test()
opts := &CheckOptions{
IO: io,
GitRoot: func() (string, error) { return dir, nil },
Runner: &installFailRunner{},
Yes: true,
JSON: true,
}
err := checkRun(opts)
if err == nil {
t.Fatal("expected error when auto-install fails")
}
if got := cmdutil.ExitCode(err); got != cmdutil.ExitError {
t.Fatalf("ExitCode = %d, want %d", got, cmdutil.ExitError)
}
var got precommit.Result
if jerr := json.Unmarshal(out.Bytes(), &got); jerr != nil {
t.Fatalf("output is not valid JSON: %v; raw=%q", jerr, out.String())
}
if got.OK || got.Reason != precommit.ReasonInstallFailed {
t.Fatalf("want ok=false reason=%q, got %+v", precommit.ReasonInstallFailed, got)
}
if len(got.InstallFailureCategories) != 1 || got.InstallFailureCategories[0] != "permission" {
t.Fatalf("want install_failure_categories=[permission], got %+v", got)
}
}
func TestCheckRunHookInstallFailureJSON(t *testing.T) {
dir := t.TempDir()
writeConfig(t, dir)
writeInstalledHookNamed(t, dir, "pre-commit")
io, _, out, _ := iostreams.Test()
opts := &CheckOptions{
IO: io,
GitRoot: func() (string, error) { return dir, nil },
Runner: hookInstallFailRunner{},
Yes: true,
JSON: true,
}
err := checkRun(opts)
if err == nil {
t.Fatal("expected error when hook installation fails")
}
if got := cmdutil.ExitCode(err); got != cmdutil.ExitError {
t.Fatalf("ExitCode = %d, want %d", got, cmdutil.ExitError)
}
var got precommit.Result
if jerr := json.Unmarshal(out.Bytes(), &got); jerr != nil {
t.Fatalf("output is not valid JSON: %v; raw=%q", jerr, out.String())
}
if got.OK || got.Reason != precommit.ReasonInstallFailed {
t.Fatalf("want ok=false reason=%q, got %+v", precommit.ReasonInstallFailed, got)
}
if !got.HookInstalled || got.HooksInstalled ||
!got.PreCommitHookInstalled || got.PrePushHookInstalled {
t.Fatalf("unexpected hook status after failed install: %+v", got)
}
}
func TestCheckRunConfigInvalidJSON(t *testing.T) {
dir := t.TempDir()
writeConfig(t, dir)
io, _, out, _ := iostreams.Test()
opts := &CheckOptions{
IO: io,
GitRoot: func() (string, error) { return dir, nil },
Runner: configInvalidRunner{},
JSON: true,
}
err := checkRun(opts)
if err == nil {
t.Fatal("expected invalid configuration to fail")
}
if got := cmdutil.ExitCode(err); got != cmdutil.ExitError {
t.Fatalf("ExitCode = %d, want %d", got, cmdutil.ExitError)
}
var got precommit.Result
if jerr := json.Unmarshal(out.Bytes(), &got); jerr != nil {
t.Fatalf("stdout is not clean JSON: %v; raw=%q", jerr, out.String())
}
if got.OK || got.Reason != precommit.ReasonConfigInvalid {
t.Fatalf("want ok=false reason=%q, got %+v", precommit.ReasonConfigInvalid, got)
}
}
func TestCheckMutuallyExclusiveFlags(t *testing.T) {
f := cmdutil.TestFactory()
cmd := NewCmdCheck(f, func(*CheckOptions) error { return nil })
cmd.SetArgs([]string{"--no-install", "--yes"})
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
if err := cmd.Execute(); err == nil {
t.Fatal("expected error when --no-install and --yes are combined")
}
}
func TestCheckRunFlagSuccess(t *testing.T) {
dir := t.TempDir()
writeConfig(t, dir)
writeInstalledHook(t, dir)
io, _, out, _ := iostreams.Test()
r := &stubRunner{version: "pre-commit 3.7.0\n"}
opts := &CheckOptions{
IO: io,
GitRoot: func() (string, error) { return dir, nil },
Runner: r,
Run: true,
}
if err := checkRun(opts); err != nil {
t.Fatalf("checkRun() error = %v", err)
}
if !strings.Contains(out.String(), "pre-commit and pre-push runs passed") {
t.Fatalf("expected both runs passed output, got %q", out.String())
}
}
func writeConfig(t *testing.T, root string) {
t.Helper()
if err := os.WriteFile(filepath.Join(root, ".pre-commit-config.yaml"), []byte("repos: []\n"), 0o644); err != nil {
t.Fatal(err)
}
}
func writeInstalledHook(t *testing.T, root string) {
t.Helper()
writeInstalledHookNamed(t, root, "pre-commit")
writeInstalledHookNamed(t, root, "pre-push")
}
func writeInstalledHookNamed(t *testing.T, root, name string) {
t.Helper()
hooks := filepath.Join(root, ".git", "hooks")
if err := os.MkdirAll(hooks, 0o755); err != nil {
t.Fatal(err)
}
content := []byte("# File generated by pre-commit\nARGS=(hook-impl --hook-type=" + name + ")\n")
if err := os.WriteFile(filepath.Join(hooks, name), content, 0o755); err != nil {
t.Fatal(err)
}
}
type stubRunner struct {
version string
runErr bool
}
func (s *stubRunner) Look(string) bool { return false }
func (s *stubRunner) Run(dir string, name string, args ...string) (string, error) {
if name == "pre-commit" && len(args) >= 1 && args[0] == "run" {
if s.runErr {
return "hook failed", errStub{}
}
return "all passed", nil
}
return s.RunStdout(dir, name, args...)
}
func (s *stubRunner) RunStdout(_ string, name string, args ...string) (string, error) {
if name == "pre-commit" && len(args) == 1 && args[0] == "--version" {
if s.version == "" {
return "", errStub{}
}
return s.version, nil
}
return "", nil
}
type errStub struct{}
func (errStub) Error() string { return "not found" }
var _ precommit.CommandRunner = (*stubRunner)(nil)
type installFailRunner struct{}
func (installFailRunner) Look(name string) bool { return name == "pipx" }
func (installFailRunner) Run(_ string, name string, _ ...string) (string, error) {
if name == "pipx" {
return "ERROR: [Errno 13] Permission denied", errStub{}
}
return "", errStub{}
}
func (installFailRunner) RunStdout(_ string, _ string, _ ...string) (string, error) {
return "", errStub{}
}
var _ precommit.CommandRunner = installFailRunner{}
type hookInstallFailRunner struct{}
func (hookInstallFailRunner) Look(string) bool { return true }
func (hookInstallFailRunner) Run(_ string, name string, args ...string) (string, error) {
if name == "pre-commit" && len(args) > 0 && args[0] == "install" {
return "permission denied", errStub{}
}
return "", nil
}
func (hookInstallFailRunner) RunStdout(_ string, name string, args ...string) (string, error) {
if name == "pre-commit" && len(args) == 1 && args[0] == "--version" {
return "pre-commit 3.7.0\n", nil
}
return "", nil
}
var _ precommit.CommandRunner = hookInstallFailRunner{}
type configInvalidRunner struct{}
func (configInvalidRunner) Look(string) bool { return true }
func (configInvalidRunner) Run(_ string, name string, args ...string) (string, error) {
if name == "pre-commit" && len(args) > 0 && args[0] == "validate-config" {
return "pre-commit version 3.2.0 is required", errStub{}
}
return "", nil
}
func (configInvalidRunner) RunStdout(_ string, name string, args ...string) (string, error) {
if name == "pre-commit" && len(args) == 1 && args[0] == "--version" {
return "pre-commit 3.1.0\n", nil
}
return "", nil
}
var _ precommit.CommandRunner = configInvalidRunner{}