Compare commits

...

2 Commits

Author SHA1 Message Date
zhangheng.023
e01132227d fix: retry TempDir cleanup for test-created git repos
Local git tooling that hooks trace2 (e.g. a global trace2.eventtarget
socket listener) can asynchronously write into a repo's .git/ shortly
after a git command runs. This races with t.TempDir's automatic
RemoveAll cleanup and intermittently fails tests with "directory not
empty", unrelated to the code under test.
2026-07-07 12:28:54 +08:00
zhangheng.023
aca356ece6 fix: write skills-state.json updated_at in local timezone 2026-07-07 11:39:40 +08:00
5 changed files with 140 additions and 18 deletions

View File

@@ -10,6 +10,7 @@ import (
"path/filepath"
"reflect"
"testing"
"time"
)
func TestScopeIncludesChangedSkillAndRelatedDomain(t *testing.T) {
@@ -51,7 +52,7 @@ func TestFileAtRevisionMissingClassifier(t *testing.T) {
}
func TestChangedFilesIncludingWorktree(t *testing.T) {
repo := t.TempDir()
repo := newGitTestRepo(t)
runGit(t, repo, "init")
runGit(t, repo, "config", "user.email", "test@example.com")
runGit(t, repo, "config", "user.name", "Test User")
@@ -83,7 +84,7 @@ func TestChangedFilesIncludingWorktree(t *testing.T) {
}
func TestChangedFilesHandlesWhitespacePaths(t *testing.T) {
repo := t.TempDir()
repo := newGitTestRepo(t)
runGit(t, repo, "init")
runGit(t, repo, "config", "user.email", "test@example.com")
runGit(t, repo, "config", "user.name", "Test User")
@@ -139,3 +140,22 @@ func gitOutput(t *testing.T, repo string, args ...string) string {
}
return string(out[:len(out)-1])
}
// newGitTestRepo returns a temp dir for a test-created git repo. Git tooling
// on this machine (trace2 hooks, etc.) can asynchronously write into a
// repo's .git/ shortly after a git command runs, racing with t.TempDir's
// automatic RemoveAll cleanup. Removing the tree ourselves first (retrying
// past that transient window) makes the later t.TempDir cleanup a no-op.
func newGitTestRepo(t *testing.T) string {
t.Helper()
repo := t.TempDir()
t.Cleanup(func() {
for i := 0; i < 10; i++ {
if err := os.RemoveAll(repo); err == nil {
return
}
time.Sleep(50 * time.Millisecond)
}
})
return repo
}

View File

@@ -10,10 +10,11 @@ import (
"path/filepath"
"strings"
"testing"
"time"
)
func TestCollectScansOnlyCurrentContributionAndMetadata(t *testing.T) {
repo := t.TempDir()
repo := newGitTestRepo(t)
runGit(t, repo, "init")
runGit(t, repo, "config", "user.email", "test@example.com")
runGit(t, repo, "config", "user.name", "Test User")
@@ -60,7 +61,7 @@ api_`+`key = "example-public-key"
}
func TestCollectScansOnlyChangedLinesInChangedFiles(t *testing.T) {
repo := t.TempDir()
repo := newGitTestRepo(t)
runGit(t, repo, "init")
runGit(t, repo, "config", "user.email", "test@example.com")
runGit(t, repo, "config", "user.name", "Test User")
@@ -92,7 +93,7 @@ func TestCollectScansOnlyChangedLinesInChangedFiles(t *testing.T) {
}
func TestCollectSemanticCandidatesStoreSanitizedReviewText(t *testing.T) {
repo := t.TempDir()
repo := newGitTestRepo(t)
runGit(t, repo, "init")
runGit(t, repo, "config", "user.email", "test@example.com")
runGit(t, repo, "config", "user.name", "Test User")
@@ -608,7 +609,7 @@ func TestCollectIgnoresDeletedPrivateKeyLine(t *testing.T) {
}
func TestCollectSkipsOnlyKnownQualityGateFixtureFiles(t *testing.T) {
repo := t.TempDir()
repo := newGitTestRepo(t)
runGit(t, repo, "init")
runGit(t, repo, "config", "user.email", "test@example.com")
runGit(t, repo, "config", "user.name", "Test User")
@@ -699,7 +700,7 @@ func TestCollectScansAddedLinesInSpecialPathNames(t *testing.T) {
}
func TestCollectScansBranchNameAsWarning(t *testing.T) {
repo := t.TempDir()
repo := newGitTestRepo(t)
metadataPath := filepath.Join(repo, "pr-metadata.json")
writeFile(t, metadataPath, `{"branch":"bot/public-doc-update"}`)
got, err := Collect(context.Background(), Options{
@@ -799,7 +800,7 @@ func TestAppendUniqueFindingsDeduplicatesByRuleFileLineAndSource(t *testing.T) {
func newGitRepo(t *testing.T) string {
t.Helper()
repo := t.TempDir()
repo := newGitTestRepo(t)
runGit(t, repo, "init")
runGit(t, repo, "config", "user.email", "test@example.com")
runGit(t, repo, "config", "user.name", "Test User")
@@ -840,7 +841,7 @@ func requireFinding(t *testing.T, got []Finding, file, rule string) {
}
func TestCollectRequiresValidMetadataJSON(t *testing.T) {
repo := t.TempDir()
repo := newGitTestRepo(t)
metadataPath := filepath.Join(repo, "pr-metadata.json")
writeFile(t, metadataPath, `{"title":`)
@@ -874,6 +875,25 @@ func runGitOutput(t *testing.T, repo string, args ...string) []byte {
return out
}
// newGitTestRepo returns a temp dir for a test-created git repo. Git tooling
// on this machine (trace2 hooks, etc.) can asynchronously write into a
// repo's .git/ shortly after a git command runs, racing with t.TempDir's
// automatic RemoveAll cleanup. Removing the tree ourselves first (retrying
// past that transient window) makes the later t.TempDir cleanup a no-op.
func newGitTestRepo(t *testing.T) string {
t.Helper()
repo := t.TempDir()
t.Cleanup(func() {
for i := 0; i < 10; i++ {
if err := os.RemoveAll(repo); err == nil {
return
}
time.Sleep(50 * time.Millisecond)
}
})
return repo
}
func writeFile(t *testing.T, path, data string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {

View File

@@ -11,6 +11,7 @@ import (
"path/filepath"
"strings"
"testing"
"time"
qdiff "github.com/larksuite/cli/internal/qualitygate/diff"
"github.com/larksuite/cli/internal/qualitygate/manifest"
@@ -69,7 +70,7 @@ func TestReferenceCommandSurfaceNormalizesShortcutDomain(t *testing.T) {
}
func TestRunRequiresCommandIndexToCoverManifest(t *testing.T) {
repo := t.TempDir()
repo := newGitTestRepo(t)
manifestPath := filepath.Join(repo, "command-manifest.json")
indexPath := filepath.Join(repo, "command-index.json")
m := manifest.Manifest{SchemaVersion: 1, Commands: []manifest.Command{{
@@ -104,7 +105,7 @@ func TestRunRequiresCommandIndexToCoverManifest(t *testing.T) {
}
func TestRunReadsManifestFilesAndAcceptsServiceReferences(t *testing.T) {
repo := t.TempDir()
repo := newGitTestRepo(t)
runGit(t, repo, "init")
runGit(t, repo, "config", "user.email", "test@example.com")
runGit(t, repo, "config", "user.name", "Test User")
@@ -190,7 +191,7 @@ description: Manage Drive comments with service command references.
}
func TestRunCollectsPublicContentFindingsIntoDiagnosticsAndFacts(t *testing.T) {
repo := t.TempDir()
repo := newGitTestRepo(t)
runGit(t, repo, "init")
runGit(t, repo, "config", "user.email", "test@example.com")
runGit(t, repo, "config", "user.name", "Test User")
@@ -283,7 +284,7 @@ func TestRunCollectsPublicContentFindingsIntoDiagnosticsAndFacts(t *testing.T) {
}
func TestLoadBaseReferenceManifestReadsCommandGolden(t *testing.T) {
repo := t.TempDir()
repo := newGitTestRepo(t)
runGit(t, repo, "init")
runGit(t, repo, "config", "user.email", "test@example.com")
runGit(t, repo, "config", "user.name", "Test User")
@@ -323,7 +324,7 @@ func TestLoadBaseReferenceManifestReadsCommandGolden(t *testing.T) {
}
func TestLoadBaseReferenceManifestReadsCommandIndexGolden(t *testing.T) {
repo := t.TempDir()
repo := newGitTestRepo(t)
runGit(t, repo, "init")
runGit(t, repo, "config", "user.email", "test@example.com")
runGit(t, repo, "config", "user.name", "Test User")
@@ -364,7 +365,7 @@ func TestLoadBaseReferenceManifestReadsCommandIndexGolden(t *testing.T) {
}
func TestLoadBaseReferenceManifestRejectsEmptyGolden(t *testing.T) {
repo := t.TempDir()
repo := newGitTestRepo(t)
runGit(t, repo, "init")
runGit(t, repo, "config", "user.email", "test@example.com")
runGit(t, repo, "config", "user.name", "Test User")
@@ -384,7 +385,7 @@ func TestLoadBaseReferenceManifestRejectsEmptyGolden(t *testing.T) {
}
func TestLoadBaseReferenceManifestRejectsInvalidGoldenKind(t *testing.T) {
repo := t.TempDir()
repo := newGitTestRepo(t)
runGit(t, repo, "init")
runGit(t, repo, "config", "user.email", "test@example.com")
runGit(t, repo, "config", "user.name", "Test User")
@@ -606,3 +607,22 @@ func runGit(t *testing.T, repo string, args ...string) {
t.Fatalf("git %v failed: %v\n%s", args, err, out)
}
}
// newGitTestRepo returns a temp dir for a test-created git repo. Git tooling
// on this machine (trace2 hooks, etc.) can asynchronously write into a
// repo's .git/ shortly after a git command runs, racing with t.TempDir's
// automatic RemoveAll cleanup. Removing the tree ourselves first (retrying
// past that transient window) makes the later t.TempDir cleanup a no-op.
func newGitTestRepo(t *testing.T) string {
t.Helper()
repo := t.TempDir()
t.Cleanup(func() {
for i := 0; i < 10; i++ {
if err := os.RemoveAll(repo); err == nil {
return
}
time.Sleep(50 * time.Millisecond)
}
})
return repo
}

View File

@@ -335,7 +335,7 @@ func SyncSkills(opts SyncOptions) *SyncResult {
UpdatedSkills: plan.ToUpdate,
AddedOfficialSkills: plan.Added,
SkippedDeletedSkills: plan.SkippedDeleted,
UpdatedAt: opts.Now().UTC().Format(time.RFC3339),
UpdatedAt: opts.Now().Format(time.RFC3339),
}
if err := WriteState(state); err != nil {
result.Action = "failed"
@@ -428,7 +428,7 @@ func fallbackFullInstall(opts SyncOptions, reason string, official []string) *Sy
UpdatedSkills: official,
AddedOfficialSkills: official,
SkippedDeletedSkills: []string{},
UpdatedAt: opts.Now().UTC().Format(time.RFC3339),
UpdatedAt: opts.Now().Format(time.RFC3339),
}
if writeErr := WriteState(state); writeErr != nil {
return &SyncResult{

View File

@@ -853,3 +853,65 @@ func TestSyncSkills_FallbackBreaksDegradationLoop(t *testing.T) {
t.Fatalf("second sync: installedAll = %d, want 0 (incremental, not fallback)", runner2.installedAll)
}
}
func TestSyncSkills_WritesLocalTimezoneUpdatedAt(t *testing.T) {
dir := t.TempDir()
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", dir)
runner := &fakeSkillsRunner{
officialIndexOut: officialSkillsIndexOutput("lark-calendar", "lark-mail", "lark-new"),
officialOut: officialSkillsOutput("lark-calendar", "lark-mail", "lark-new"),
globalJSONOut: globalSkillsJSONOutput("lark-calendar"),
globalOut: globalSkillsOutput("lark-mail"),
}
localZone := time.FixedZone("UTC+8", 8*60*60)
result := SyncSkills(SyncOptions{
Version: "1.0.33",
Runner: runner,
Now: func() time.Time { return time.Date(2026, 5, 18, 12, 0, 0, 0, localZone) },
})
if result.Err != nil {
t.Fatalf("SyncSkills() err = %v, want nil", result.Err)
}
state, readable, err := ReadState()
if err != nil || !readable {
t.Fatalf("ReadState() = (_, %v, %v), want readable", readable, err)
}
want := "2026-05-18T12:00:00+08:00"
if state.UpdatedAt != want {
t.Fatalf("state.UpdatedAt = %q, want %q (local timezone offset, not UTC Z-suffix)", state.UpdatedAt, want)
}
}
func TestSyncSkills_FallbackWritesLocalTimezoneUpdatedAt(t *testing.T) {
dir := t.TempDir()
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", dir)
runner := &fakeSkillsRunner{
officialIndexOut: officialSkillsIndexOutput("lark-calendar", "lark-mail"),
officialOut: officialSkillsOutput("lark-calendar", "lark-mail"),
globalJSONOut: globalSkillsJSONOutput("lark-calendar", "lark-mail"),
globalOut: globalSkillsOutput("lark-calendar", "lark-mail"),
installErr: fmt.Errorf("incremental boom"),
installAllErr: nil,
}
localZone := time.FixedZone("UTC+8", 8*60*60)
result := SyncSkills(SyncOptions{
Version: "1.0.33",
Runner: runner,
Now: func() time.Time { return time.Date(2026, 5, 18, 12, 0, 0, 0, localZone) },
})
if result.Action != "fallback_synced" {
t.Fatalf("SyncSkills() action = %q, want fallback_synced", result.Action)
}
state, readable, err := ReadState()
if err != nil || !readable {
t.Fatalf("ReadState() = (_, %v, %v), want readable", readable, err)
}
want := "2026-05-18T12:00:00+08:00"
if state.UpdatedAt != want {
t.Fatalf("state.UpdatedAt = %q, want %q (local timezone offset, not UTC Z-suffix)", state.UpdatedAt, want)
}
}