diff --git a/AGENTS.md b/AGENTS.md index 443730b..04ae98f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -175,7 +175,7 @@ The real implementation calls GitHub's REST API. Tests use a mock implementation At minimum, every rule must have: - A passing case (repo satisfies the rule) - A failing case (repo violates the rule) -- An edge case where applicable (e.g., README exists but is under 100 lines) +- An edge case where applicable (e.g., README exists but is under 2KB) Report generation tests must verify the Markdown output matches expected structure. diff --git a/README.md b/README.md index 02e3306..5fa1097 100644 --- a/README.md +++ b/README.md @@ -38,12 +38,12 @@ Each rule produces a **pass** or **fail** result per repository. There are no sc **Pass:** file found. **Fail:** file not found. -#### 3. Has README over 100 lines +#### 3. Has README over 2KB -**Check:** a `README.md` file exists in the repo root and contains more than 100 lines. +**Check:** a `README.md` file exists in the repo root and is larger than 2048 bytes. -**Pass:** `README.md` exists and has >100 lines. -**Fail:** `README.md` is missing, or exists but has ≤100 lines. +**Pass:** `README.md` exists and is >2048 bytes. +**Fail:** `README.md` is missing, or exists but is ≤2048 bytes. #### 4. Has LICENSE @@ -134,7 +134,7 @@ The report is a single Markdown document posted as a GitHub Issue. Structure: |------|--------| | Has repo description | ✅ | | Has .gitignore | ✅ | -| Has README over 100 lines | ❌ | +| Has README over 2KB | ❌ | | ... | ... | ### repo-name-2 diff --git a/client.go b/client.go index 0fece2f..e7ee424 100644 --- a/client.go +++ b/client.go @@ -8,20 +8,26 @@ import ( "net/http" ) +// FileEntry represents a file or directory in a repo's root. +type FileEntry struct { + Name string + Size int +} + // Repo represents a GitHub repository with the fields the scanner needs. type Repo struct { Name string Description string DefaultBranch string Archived bool - Files []string // root-level file and directory names + Files []FileEntry // root-level file and directory entries } // GitHubClient is the interface for all GitHub API interactions. // The scanner depends only on this interface, making it testable via mocks. type GitHubClient interface { ListRepos(ctx context.Context, org string) ([]Repo, error) - ListFiles(ctx context.Context, owner, repo string) ([]string, error) + ListFiles(ctx context.Context, owner, repo string) ([]FileEntry, error) CreateIssue(ctx context.Context, owner, repo, title, body string) error } @@ -102,21 +108,22 @@ func (c *realGitHubClient) ListRepos(ctx context.Context, org string) ([]Repo, e return allRepos, nil } -func (c *realGitHubClient) ListFiles(ctx context.Context, owner, repo string) ([]string, error) { +func (c *realGitHubClient) ListFiles(ctx context.Context, owner, repo string) ([]FileEntry, error) { url := fmt.Sprintf("https://api.github.com/repos/%s/%s/contents", owner, repo) var entries []struct { Name string `json:"name"` + Size int `json:"size"` } if err := c.doRequest(ctx, url, &entries); err != nil { return nil, fmt.Errorf("list files for %s/%s: %w", owner, repo, err) } - names := make([]string, len(entries)) + files := make([]FileEntry, len(entries)) for i, e := range entries { - names[i] = e.Name + files[i] = FileEntry{Name: e.Name, Size: e.Size} } - return names, nil + return files, nil } func (c *realGitHubClient) CreateIssue(ctx context.Context, owner, repo, title, body string) error { diff --git a/client_mock_test.go b/client_mock_test.go index 93a98ea..7a17cb4 100644 --- a/client_mock_test.go +++ b/client_mock_test.go @@ -6,7 +6,7 @@ import "context" type MockGitHubClient struct { Repos []Repo Err error - Files map[string][]string // repo name -> file list + Files map[string][]FileEntry // repo name -> file entries FilesErr error IssueErr error // CreatedIssue records the last CreateIssue call for assertions. @@ -19,7 +19,7 @@ func (m *MockGitHubClient) ListRepos(ctx context.Context, org string) ([]Repo, e return m.Repos, m.Err } -func (m *MockGitHubClient) ListFiles(ctx context.Context, owner, repo string) ([]string, error) { +func (m *MockGitHubClient) ListFiles(ctx context.Context, owner, repo string) ([]FileEntry, error) { if m.FilesErr != nil { return nil, m.FilesErr } diff --git a/rules.go b/rules.go index 09fe4ba..61347d5 100644 --- a/rules.go +++ b/rules.go @@ -19,6 +19,7 @@ func AllRules() []Rule { return []Rule{ HasRepoDescription{}, HasGitignore{}, + HasSubstantialReadme{}, } } @@ -38,11 +39,25 @@ func (r HasGitignore) Check(repo Repo) bool { return hasFile(repo.Files, ".gitignore") } -func hasFile(files []string, name string) bool { +// HasSubstantialReadme checks that README.md exists and is larger than 2048 bytes. +type HasSubstantialReadme struct{} + +func (r HasSubstantialReadme) Name() string { return "Has README over 2KB" } +func (r HasSubstantialReadme) Check(repo Repo) bool { + f, ok := findFile(repo.Files, "README.md") + return ok && f.Size > 2048 +} + +func findFile(files []FileEntry, name string) (FileEntry, bool) { for _, f := range files { - if f == name { - return true + if f.Name == name { + return f, true } } - return false + return FileEntry{}, false +} + +func hasFile(files []FileEntry, name string) bool { + _, ok := findFile(files, name) + return ok } diff --git a/rules_test.go b/rules_test.go index c256265..0f27679 100644 --- a/rules_test.go +++ b/rules_test.go @@ -29,7 +29,7 @@ func TestHasRepoDescription_Fail_WhitespaceOnly(t *testing.T) { func TestHasGitignore_Pass(t *testing.T) { rule := HasGitignore{} - if !rule.Check(Repo{Files: []string{"README.md", ".gitignore", "main.go"}}) { + if !rule.Check(Repo{Files: []FileEntry{{Name: "README.md"}, {Name: ".gitignore"}, {Name: "main.go"}}}) { t.Error("expected pass when .gitignore exists") } } @@ -37,7 +37,7 @@ func TestHasGitignore_Pass(t *testing.T) { func TestHasGitignore_Fail(t *testing.T) { rule := HasGitignore{} - if rule.Check(Repo{Files: []string{"README.md", "main.go"}}) { + if rule.Check(Repo{Files: []FileEntry{{Name: "README.md"}, {Name: "main.go"}}}) { t.Error("expected fail when .gitignore is missing") } } @@ -49,3 +49,27 @@ func TestHasGitignore_Fail_EmptyFiles(t *testing.T) { t.Error("expected fail when file list is empty") } } + +func TestHasSubstantialReadme_Pass(t *testing.T) { + rule := HasSubstantialReadme{} + + if !rule.Check(Repo{Files: []FileEntry{{Name: "README.md", Size: 3000}}}) { + t.Error("expected pass for README.md over 2KB") + } +} + +func TestHasSubstantialReadme_Fail_TooSmall(t *testing.T) { + rule := HasSubstantialReadme{} + + if rule.Check(Repo{Files: []FileEntry{{Name: "README.md", Size: 2048}}}) { + t.Error("expected fail for README.md exactly 2048 bytes") + } +} + +func TestHasSubstantialReadme_Fail_Missing(t *testing.T) { + rule := HasSubstantialReadme{} + + if rule.Check(Repo{Files: []FileEntry{{Name: "main.go"}}}) { + t.Error("expected fail when README.md is missing") + } +}