diff --git a/internal/config/config.go b/internal/config/config.go index d6f9e2c..2f2d369 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -67,6 +67,9 @@ func Validate(cfg domain.Config) error { if strings.TrimSpace(cfg.Gitea.Org) == "" { return errors.New("gitea org is required") } + if strings.TrimSpace(cfg.Gitea.Username) == "" { + return errors.New("gitea username is required") + } if cfg.Gitea.AuthType != AuthPassword && cfg.Gitea.AuthType != AuthToken { return errors.New("authType must be password or token") } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 82fe683..60a9c9b 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -62,6 +62,11 @@ func TestValidateRejectsMissingConnectionFields(t *testing.T) { } cfg.Gitea.Org = "skills" + if err := Validate(cfg); err == nil { + t.Fatal("Validate returned nil error without username") + } + + cfg.Gitea.Username = "alice" if err := Validate(cfg); err != nil { t.Fatalf("Validate returned error for valid config: %v", err) } diff --git a/internal/service/service.go b/internal/service/service.go index f5a2732..a6dfc31 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -375,11 +375,13 @@ func (s *Service) RunAutoUpdate(ctx context.Context) error { state.Skills[i] = skill continue } - if commit, err := s.git.CurrentCommit(skill.LocalPath); err == nil { - skill.CurrentCommit = commit - } else { + commit, err := s.git.CurrentCommit(skill.LocalPath) + if err != nil { skill.LastError = err.Error() + state.Skills[i] = skill + continue } + skill.CurrentCommit = commit skill.LastError = "" skill.LastCheckedAt = time.Now().Format(time.RFC3339) state.Skills[i] = skill diff --git a/internal/service/service_test.go b/internal/service/service_test.go index 85aa250..8858c2d 100644 --- a/internal/service/service_test.go +++ b/internal/service/service_test.go @@ -180,6 +180,37 @@ func TestRunAutoUpdateSkipsDirtyRepositories(t *testing.T) { } } +func TestRunAutoUpdateKeepsCurrentCommitError(t *testing.T) { + svc, _, _, _, git, _ := newTestService(t) + saveValidConfig(t, svc) + local := filepath.Join(t.TempDir(), "demo") + state := domain.State{Skills: []domain.SkillState{{ + Org: "skills", + Repo: "demo", + LocalPath: local, + DefaultBranch: "main", + CurrentCommit: "old", + }}} + if err := svc.store.Save(state); err != nil { + t.Fatal(err) + } + git.remoteCommit[local] = "new" + git.currentErr[local] = errors.New("rev-parse failed") + + if err := svc.RunAutoUpdate(context.Background()); err != nil { + t.Fatalf("RunAutoUpdate returned error: %v", err) + } + + reloaded, err := svc.store.Load() + if err != nil { + t.Fatal(err) + } + saved, _ := skillstore.FindSkill(reloaded, "skills", "demo") + if saved.LastError != "rev-parse failed" { + t.Fatalf("LastError = %q, want rev-parse failed", saved.LastError) + } +} + func newTestService(t *testing.T) (*Service, Paths, *fakeSecretStore, *fakeRemoteClient, *fakeGit, *fakeTargets) { t.Helper() dir := t.TempDir() @@ -192,7 +223,7 @@ func newTestService(t *testing.T) (*Service, Paths, *fakeSecretStore, *fakeRemot } secrets := &fakeSecretStore{values: map[string]string{}} remote := &fakeRemoteClient{} - git := &fakeGit{dirty: map[string]bool{}, currentCommit: map[string]string{}, remoteCommit: map[string]string{}} + git := &fakeGit{dirty: map[string]bool{}, currentCommit: map[string]string{}, remoteCommit: map[string]string{}, currentErr: map[string]error{}} target := &fakeTargets{} svc := New(Options{ Paths: paths, @@ -263,6 +294,7 @@ type fakeGit struct { dirty map[string]bool currentCommit map[string]string remoteCommit map[string]string + currentErr map[string]error pullCalls int } @@ -272,6 +304,9 @@ func (f *fakeGit) Pull(context.Context, string, gitops.Credentials) error { return nil } func (f *fakeGit) CurrentCommit(path string) (string, error) { + if err := f.currentErr[path]; err != nil { + return "", err + } if commit := f.currentCommit[path]; commit != "" { return commit, nil }