From c76904c3560a6c1f5a261e0f43f4986d5893e24a Mon Sep 17 00:00:00 2001 From: luozhixiong Date: Sat, 18 Jul 2026 16:30:50 +0800 Subject: [PATCH] test: synchronize temporary Git maintenance --- internal/qualitygate/diff/diff_test.go | 9 ++- .../qualitygate/publiccontent/collect_test.go | 9 ++- internal/qualitygate/rules/run_test.go | 5 +- internal/testutil/gitcmd/gitcmd.go | 55 +++++++++++++++++++ internal/testutil/gitcmd/gitcmd_test.go | 47 ++++++++++++++++ lint/errscontract/scan_test.go | 15 ++++- shortcuts/apps/apps_init_test.go | 6 +- 7 files changed, 131 insertions(+), 15 deletions(-) create mode 100644 internal/testutil/gitcmd/gitcmd.go create mode 100644 internal/testutil/gitcmd/gitcmd_test.go diff --git a/internal/qualitygate/diff/diff_test.go b/internal/qualitygate/diff/diff_test.go index 71b1173010..2b5f88831a 100644 --- a/internal/qualitygate/diff/diff_test.go +++ b/internal/qualitygate/diff/diff_test.go @@ -6,10 +6,11 @@ package diff import ( "context" "os" - "os/exec" "path/filepath" "reflect" "testing" + + "github.com/larksuite/cli/internal/testutil/gitcmd" ) func TestScopeIncludesChangedSkillAndRelatedDomain(t *testing.T) { @@ -122,8 +123,7 @@ func writeFile(t *testing.T, repo, rel, content string) { func runGit(t *testing.T, repo string, args ...string) { t.Helper() - cmd := exec.Command("git", args...) - cmd.Dir = repo + cmd := gitcmd.Command(repo, args...) if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("git %v failed: %v\n%s", args, err, out) } @@ -131,8 +131,7 @@ func runGit(t *testing.T, repo string, args ...string) { func gitOutput(t *testing.T, repo string, args ...string) string { t.Helper() - cmd := exec.Command("git", args...) - cmd.Dir = repo + cmd := gitcmd.Command(repo, args...) out, err := cmd.Output() if err != nil { t.Fatalf("git %v failed: %v", args, err) diff --git a/internal/qualitygate/publiccontent/collect_test.go b/internal/qualitygate/publiccontent/collect_test.go index 5ea92779f7..332ee4e3b8 100644 --- a/internal/qualitygate/publiccontent/collect_test.go +++ b/internal/qualitygate/publiccontent/collect_test.go @@ -6,10 +6,11 @@ package publiccontent import ( "context" "os" - "os/exec" "path/filepath" "strings" "testing" + + "github.com/larksuite/cli/internal/testutil/gitcmd" ) func TestCollectScansOnlyCurrentContributionAndMetadata(t *testing.T) { @@ -855,8 +856,7 @@ func runGit(t *testing.T, repo string, args ...string) { if len(args) > 0 && args[0] == "commit" { args = append([]string{"commit", "--no-verify"}, args[1:]...) } - cmd := exec.Command("git", args...) - cmd.Dir = repo + cmd := gitcmd.Command(repo, args...) out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("git %v failed: %v\n%s", args, err, out) @@ -865,8 +865,7 @@ func runGit(t *testing.T, repo string, args ...string) { func runGitOutput(t *testing.T, repo string, args ...string) []byte { t.Helper() - cmd := exec.Command("git", args...) - cmd.Dir = repo + cmd := gitcmd.Command(repo, args...) out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("git %v failed: %v\n%s", args, err, out) diff --git a/internal/qualitygate/rules/run_test.go b/internal/qualitygate/rules/run_test.go index b60a2c6322..7b13dac633 100644 --- a/internal/qualitygate/rules/run_test.go +++ b/internal/qualitygate/rules/run_test.go @@ -7,7 +7,6 @@ import ( "context" "encoding/json" "os" - "os/exec" "path/filepath" "strings" "testing" @@ -15,6 +14,7 @@ import ( qdiff "github.com/larksuite/cli/internal/qualitygate/diff" "github.com/larksuite/cli/internal/qualitygate/manifest" "github.com/larksuite/cli/internal/qualitygate/report" + "github.com/larksuite/cli/internal/testutil/gitcmd" "github.com/larksuite/cli/internal/vfs" ) @@ -599,7 +599,8 @@ func TestNormalizeDiagnosticFileHandlesAbsoluteRepo(t *testing.T) { func runGit(t *testing.T, repo string, args ...string) { t.Helper() - cmd := exec.Command("git", append([]string{"-c", "core.hooksPath=/dev/null", "-C", repo}, args...)...) + commandArgs := append([]string{"-c", "core.hooksPath=/dev/null"}, args...) + cmd := gitcmd.Command(repo, commandArgs...) cmd.Env = append(os.Environ(), "GIT_AUTHOR_DATE=2026-06-17T00:00:00Z", "GIT_COMMITTER_DATE=2026-06-17T00:00:00Z") out, err := cmd.CombinedOutput() if err != nil { diff --git a/internal/testutil/gitcmd/gitcmd.go b/internal/testutil/gitcmd/gitcmd.go new file mode 100644 index 0000000000..e878ed486c --- /dev/null +++ b/internal/testutil/gitcmd/gitcmd.go @@ -0,0 +1,55 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +// Package gitcmd provides Git process helpers for tests that use temporary +// repositories. +package gitcmd + +import ( + "os" + "os/exec" + "strconv" + "testing" +) + +const ( + maintenanceAutoDetach = "maintenance.autoDetach" + gcAutoDetach = "gc.autoDetach" +) + +// Command creates a Git command whose automatic maintenance stays in the +// command lifecycle, so temporary repository cleanup cannot race a detached +// maintenance process. +func Command(dir string, args ...string) *exec.Cmd { + commandArgs := make([]string, 0, len(args)+4) + commandArgs = append(commandArgs, + "-c", maintenanceAutoDetach+"=false", + "-c", gcAutoDetach+"=false", + ) + commandArgs = append(commandArgs, args...) + cmd := exec.Command("git", commandArgs...) + cmd.Dir = dir + return cmd +} + +// SetSynchronousMaintenanceEnv applies the same lifecycle contract to every +// Git process started by the current test, including processes created through +// production command runners. Tests using it must not run in parallel. +func SetSynchronousMaintenanceEnv(t *testing.T) { + t.Helper() + count := 0 + if value, ok := os.LookupEnv("GIT_CONFIG_COUNT"); ok { + parsed, err := strconv.Atoi(value) + if err != nil || parsed < 0 { + t.Fatalf("invalid GIT_CONFIG_COUNT %q", value) + } + count = parsed + } + for _, key := range []string{maintenanceAutoDetach, gcAutoDetach} { + index := strconv.Itoa(count) + t.Setenv("GIT_CONFIG_KEY_"+index, key) + t.Setenv("GIT_CONFIG_VALUE_"+index, "false") + count++ + } + t.Setenv("GIT_CONFIG_COUNT", strconv.Itoa(count)) +} diff --git a/internal/testutil/gitcmd/gitcmd_test.go b/internal/testutil/gitcmd/gitcmd_test.go new file mode 100644 index 0000000000..96abe1648d --- /dev/null +++ b/internal/testutil/gitcmd/gitcmd_test.go @@ -0,0 +1,47 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package gitcmd + +import ( + "os/exec" + "strings" + "testing" +) + +func TestCommandDisablesDetachedMaintenance(t *testing.T) { + for _, key := range []string{"maintenance.autoDetach", "gc.autoDetach"} { + cmd := Command(t.TempDir(), "config", "--get", "--type=bool", key) + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("git config %s: %v\n%s", key, err, out) + } + if got := strings.TrimSpace(string(out)); got != "false" { + t.Fatalf("%s = %q, want false", key, got) + } + } +} + +func TestSetSynchronousMaintenanceEnv(t *testing.T) { + t.Setenv("GIT_CONFIG_COUNT", "1") + t.Setenv("GIT_CONFIG_KEY_0", "user.name") + t.Setenv("GIT_CONFIG_VALUE_0", "Existing Test User") + SetSynchronousMaintenanceEnv(t) + for key, want := range map[string]string{ + "user.name": "Existing Test User", + maintenanceAutoDetach: "false", + gcAutoDetach: "false", + } { + cmd := exec.Command("git", "config", "--get", "--type=bool", key) + if key == "user.name" { + cmd = exec.Command("git", "config", "--get", key) + } + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("git config %s: %v\n%s", key, err, out) + } + if got := strings.TrimSpace(string(out)); got != want { + t.Fatalf("%s = %q, want %q", key, got, want) + } + } +} diff --git a/lint/errscontract/scan_test.go b/lint/errscontract/scan_test.go index 686fe3c018..d2b2b9b8f1 100644 --- a/lint/errscontract/scan_test.go +++ b/lint/errscontract/scan_test.go @@ -34,7 +34,12 @@ func writeFixture(t *testing.T, files fixtureRepo) string { func runGit(t *testing.T, root string, args ...string) string { t.Helper() - cmd := exec.Command("git", args...) + commandArgs := []string{ + "-c", "maintenance.autoDetach=false", + "-c", "gc.autoDetach=false", + } + commandArgs = append(commandArgs, args...) + cmd := exec.Command("git", commandArgs...) cmd.Dir = root out, err := cmd.CombinedOutput() if err != nil { @@ -43,6 +48,14 @@ func runGit(t *testing.T, root string, args ...string) string { return strings.TrimSpace(string(out)) } +func TestRunGitDisablesDetachedMaintenance(t *testing.T) { + for _, key := range []string{"maintenance.autoDetach", "gc.autoDetach"} { + if got := runGit(t, t.TempDir(), "config", "--get", "--type=bool", key); got != "false" { + t.Fatalf("%s = %q, want false", key, got) + } + } +} + func TestLoadSubtypeAllowlist_ExtractsTypedConstValues(t *testing.T) { root := writeFixture(t, fixtureRepo{ "errs/subtypes.go": `package errs diff --git a/shortcuts/apps/apps_init_test.go b/shortcuts/apps/apps_init_test.go index 26684b193d..0b62aa39dd 100644 --- a/shortcuts/apps/apps_init_test.go +++ b/shortcuts/apps/apps_init_test.go @@ -21,6 +21,7 @@ import ( "github.com/larksuite/cli/internal/cmdutil" "github.com/larksuite/cli/internal/core" "github.com/larksuite/cli/internal/httpmock" + "github.com/larksuite/cli/internal/testutil/gitcmd" "github.com/larksuite/cli/shortcuts/common" ) @@ -929,8 +930,7 @@ func TestAppsInit_NonEmpty_SingleInitCommit(t *testing.T) { // gitMust runs a git command in dir with a real binary, failing the test on error. func gitMust(t *testing.T, dir string, args ...string) string { t.Helper() - cmd := exec.Command("git", args...) - cmd.Dir = dir + cmd := gitcmd.Command(dir, args...) out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("git %v in %s failed: %v\n%s", args, dir, err, out) @@ -946,6 +946,7 @@ func TestCommitAndPushIfDirty_RealGit_IgnoredAgentDir(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git not available") } + gitcmd.SetSynchronousMaintenanceEnv(t) // Bare remote so `git push origin sprint/default` succeeds. remote := t.TempDir() gitMust(t, remote, "init", "--bare", "-q", "--initial-branch", defaultInitBranch) @@ -1067,6 +1068,7 @@ func TestCommitAndPushIfDirty_RealGit_NonEmptyUpgrade(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git not available") } + gitcmd.SetSynchronousMaintenanceEnv(t) remote := t.TempDir() gitMust(t, remote, "init", "--bare", "-q", "--initial-branch", defaultInitBranch)