mirror of
https://github.com/tiennm99/awesome-coding-agents.git
synced 2026-10-03 13:12:01 +00:00
350 lines
11 KiB
Go
350 lines
11 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// aliasRe extracts the numeric alias index from a GraphQL query fragment
|
|
// like " r17: repository(owner: \"foo\", name: \"bar\") {...}".
|
|
var aliasRe = regexp.MustCompile(`r(\d+):\s*repository`)
|
|
|
|
// withGraphQLURL points graphqlURL at srv for the duration of the test and
|
|
// restores the original value on cleanup.
|
|
func withGraphQLURL(t *testing.T, srv *httptest.Server) {
|
|
t.Helper()
|
|
orig := graphqlURL
|
|
graphqlURL = srv.URL
|
|
t.Cleanup(func() { graphqlURL = orig })
|
|
}
|
|
|
|
func readGraphQLQuery(t *testing.T, r *http.Request) string {
|
|
t.Helper()
|
|
body, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
t.Fatalf("read request body: %v", err)
|
|
}
|
|
var req struct {
|
|
Query string `json:"query"`
|
|
}
|
|
if err := json.Unmarshal(body, &req); err != nil {
|
|
t.Fatalf("unmarshal request body: %v (body=%s)", err, body)
|
|
}
|
|
return req.Query
|
|
}
|
|
|
|
func TestFetchStats_ChunkingHappyPath(t *testing.T) {
|
|
// 55 agents forces 2 chunks (50 + 5) — chunk-boundary code has zero
|
|
// production coverage today (only 29 agents exist), so this is the
|
|
// first exercise of the alias-offset math across a chunk boundary.
|
|
const n = 55
|
|
agents := make([]Agent, n)
|
|
for i := range agents {
|
|
agents[i] = Agent{Owner: "org", Repo: fmt.Sprintf("repo%02d", i), Tags: []string{"terminal", "community"}}
|
|
}
|
|
// A curated description in agents.yml replaces the GitHub one.
|
|
agents[3].Description = "curated"
|
|
|
|
var requestCount int
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
requestCount++
|
|
query := readGraphQLQuery(t, r)
|
|
|
|
data := map[string]any{}
|
|
for _, m := range aliasRe.FindAllStringSubmatch(query, -1) {
|
|
idx, err := strconv.Atoi(m[1])
|
|
if err != nil {
|
|
t.Fatalf("parse alias index from %q: %v", m[0], err)
|
|
}
|
|
data["r"+m[1]] = map[string]any{
|
|
"stargazerCount": 100 + idx,
|
|
"description": "desc " + m[1],
|
|
"primaryLanguage": map[string]string{"name": "Go"},
|
|
"pushedAt": "2026-08-01T00:00:00Z",
|
|
"url": fmt.Sprintf("https://github.com/org/repo%02d", idx),
|
|
"nameWithOwner": fmt.Sprintf("org/repo%02d", idx),
|
|
"isArchived": false,
|
|
}
|
|
}
|
|
out, err := json.Marshal(map[string]any{"data": data})
|
|
if err != nil {
|
|
t.Fatalf("marshal response: %v", err)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if _, err := w.Write(out); err != nil {
|
|
t.Fatalf("write response: %v", err)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
withGraphQLURL(t, srv)
|
|
|
|
stats, err := fetchStats("test-token", agents)
|
|
if err != nil {
|
|
t.Fatalf("fetchStats: %v", err)
|
|
}
|
|
if requestCount != 2 {
|
|
t.Errorf("expected 2 chunk requests (50 + 5), got %d", requestCount)
|
|
}
|
|
if len(stats) != n {
|
|
t.Fatalf("expected %d stats, got %d", n, len(stats))
|
|
}
|
|
// Sorted descending by stars: repo54 (154 stars) must be first.
|
|
if stats[0].NameWithOwner != "org/repo54" || stats[0].Stars != 154 {
|
|
t.Errorf("expected top stat org/repo54 with 154 stars, got %s with %d", stats[0].NameWithOwner, stats[0].Stars)
|
|
}
|
|
// Spot-check a repo from the second chunk (alias offset 50) resolved
|
|
// correctly — this is exactly the code path that has never run in
|
|
// production (only 1 chunk is used with 29 agents).
|
|
var found bool
|
|
for _, s := range stats {
|
|
if s.NameWithOwner == "org/repo52" {
|
|
found = true
|
|
if s.Stars != 152 {
|
|
t.Errorf("org/repo52: expected 152 stars, got %d", s.Stars)
|
|
}
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("expected org/repo52 (second chunk) in results")
|
|
}
|
|
for _, s := range stats {
|
|
switch s.NameWithOwner {
|
|
case "org/repo03":
|
|
if s.Description != "curated" {
|
|
t.Errorf("org/repo03: expected curated description, got %q", s.Description)
|
|
}
|
|
case "org/repo04":
|
|
if s.Description != "desc 4" {
|
|
t.Errorf("org/repo04: expected GitHub description, got %q", s.Description)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFetchStats_MissingNodeNamesTheRepo(t *testing.T) {
|
|
agents := []Agent{
|
|
{Owner: "foo", Repo: "bar0", Tags: []string{"terminal", "community"}},
|
|
{Owner: "foo", Repo: "bar1", Tags: []string{"terminal", "community"}},
|
|
{Owner: "foo", Repo: "bar2", Tags: []string{"terminal", "community"}},
|
|
}
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
query := readGraphQLQuery(t, r)
|
|
data := map[string]any{}
|
|
for _, m := range aliasRe.FindAllStringSubmatch(query, -1) {
|
|
if m[1] == "2" {
|
|
continue // simulate repo r2 (foo/bar2) deleted/private/renamed away
|
|
}
|
|
data["r"+m[1]] = map[string]any{
|
|
"stargazerCount": 10,
|
|
"nameWithOwner": "foo/bar" + m[1],
|
|
}
|
|
}
|
|
out, _ := json.Marshal(map[string]any{"data": data}) // static map of strings/ints/bools never fails to marshal
|
|
_, _ = w.Write(out) // httptest ResponseRecorder write error is not actionable in a test fake
|
|
}))
|
|
defer srv.Close()
|
|
withGraphQLURL(t, srv)
|
|
|
|
_, err := fetchStats("test-token", agents)
|
|
if err == nil {
|
|
t.Fatal("expected error for missing node, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "foo/bar2") {
|
|
t.Errorf("expected error to name the missing repo foo/bar2, got: %v", err)
|
|
}
|
|
if !strings.Contains(err.Error(), "alias r2") {
|
|
t.Errorf("expected error to name the alias r2, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestFetchStats_GraphQLErrorNamesTheRepo(t *testing.T) {
|
|
agents := []Agent{
|
|
{Owner: "foo", Repo: "bar0", Tags: []string{"terminal", "community"}},
|
|
{Owner: "foo", Repo: "bar1", Tags: []string{"terminal", "community"}},
|
|
{Owner: "foo", Repo: "bar2", Tags: []string{"terminal", "community"}},
|
|
}
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
resp := map[string]any{
|
|
"data": nil,
|
|
"errors": []map[string]any{
|
|
{"message": "Could not resolve to a Repository.", "path": []any{"r2"}},
|
|
},
|
|
}
|
|
out, _ := json.Marshal(resp) // static map of strings/ints/bools never fails to marshal
|
|
_, _ = w.Write(out) // httptest ResponseRecorder write error is not actionable in a test fake
|
|
}))
|
|
defer srv.Close()
|
|
withGraphQLURL(t, srv)
|
|
|
|
_, err := fetchStats("test-token", agents)
|
|
if err == nil {
|
|
t.Fatal("expected error for GraphQL-level error, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "repo foo/bar2") {
|
|
t.Errorf("expected error to name repo foo/bar2, got: %v", err)
|
|
}
|
|
if !strings.Contains(err.Error(), "alias r2") {
|
|
t.Errorf("expected error to name alias r2, got: %v", err)
|
|
}
|
|
if !strings.Contains(err.Error(), "Could not resolve to a Repository.") {
|
|
t.Errorf("expected original GraphQL message preserved, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestFetchStats_DriftWarnings(t *testing.T) {
|
|
// Renamed and archived repos should not error the run — fetchStats
|
|
// still succeeds and reports the drift as a warning (checked via
|
|
// captured stdout) rather than failing the whole daily update.
|
|
agents := []Agent{
|
|
{Owner: "old-owner", Repo: "renamed-repo", Tags: []string{"terminal", "community"}},
|
|
{Owner: "org", Repo: "archived-repo", Tags: []string{"terminal", "community"}},
|
|
}
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
data := map[string]any{
|
|
"r0": map[string]any{
|
|
"stargazerCount": 5,
|
|
"nameWithOwner": "new-owner/renamed-repo",
|
|
"isArchived": false,
|
|
},
|
|
"r1": map[string]any{
|
|
"stargazerCount": 5,
|
|
"nameWithOwner": "org/archived-repo",
|
|
"isArchived": true,
|
|
},
|
|
}
|
|
out, _ := json.Marshal(map[string]any{"data": data}) // static map of strings/ints/bools never fails to marshal
|
|
_, _ = w.Write(out) // httptest ResponseRecorder write error is not actionable in a test fake
|
|
}))
|
|
defer srv.Close()
|
|
withGraphQLURL(t, srv)
|
|
|
|
stats, err := fetchStats("test-token", agents)
|
|
if err != nil {
|
|
t.Fatalf("fetchStats: %v (drift should warn, not fail)", err)
|
|
}
|
|
if len(stats) != 2 {
|
|
t.Fatalf("expected 2 stats, got %d", len(stats))
|
|
}
|
|
for _, s := range stats {
|
|
if s.CanonicalKey == "org/archived-repo" && !s.IsArchived {
|
|
t.Error("expected org/archived-repo Stat.IsArchived=true")
|
|
}
|
|
}
|
|
}
|
|
|
|
// captureStdout redirects os.Stdout for the duration of fn and returns what
|
|
// was written, so the ::warning:: annotations fetchStats emits are assertable.
|
|
func captureStdout(t *testing.T, fn func()) string {
|
|
t.Helper()
|
|
r, w, err := os.Pipe()
|
|
if err != nil {
|
|
t.Fatalf("os.Pipe: %v", err)
|
|
}
|
|
orig := os.Stdout
|
|
os.Stdout = w
|
|
defer func() { os.Stdout = orig }()
|
|
|
|
fn()
|
|
|
|
if err := w.Close(); err != nil {
|
|
t.Fatalf("close pipe writer: %v", err)
|
|
}
|
|
out, err := io.ReadAll(r)
|
|
if err != nil {
|
|
t.Fatalf("read pipe: %v", err)
|
|
}
|
|
return string(out)
|
|
}
|
|
|
|
func TestFetchStats_StaleWarning(t *testing.T) {
|
|
fixed := time.Date(2026, 9, 11, 0, 0, 0, 0, time.UTC)
|
|
origNow := timeNow
|
|
timeNow = func() time.Time { return fixed }
|
|
defer func() { timeNow = origNow }()
|
|
|
|
agents := []Agent{
|
|
{Owner: "org", Repo: "fresh", Tags: []string{"terminal", "community"}},
|
|
{Owner: "org", Repo: "stale", Tags: []string{"terminal", "community"}},
|
|
{Owner: "org", Repo: "stale-and-archived", Tags: []string{"terminal", "community"}},
|
|
}
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
node := func(name, pushedAt string, archived bool) map[string]any {
|
|
return map[string]any{
|
|
"stargazerCount": 5,
|
|
"nameWithOwner": name,
|
|
"pushedAt": pushedAt,
|
|
"isArchived": archived,
|
|
}
|
|
}
|
|
out, _ := json.Marshal(map[string]any{"data": map[string]any{
|
|
// 2 days idle: inside the window, no warning.
|
|
"r0": node("org/fresh", "2026-09-09T00:00:00Z", false),
|
|
// 120 days idle: past staleWarnAfter.
|
|
"r1": node("org/stale", "2026-05-14T00:00:00Z", false),
|
|
// Also stale, but archived — the archived warning covers it.
|
|
"r2": node("org/stale-and-archived", "2025-01-01T00:00:00Z", true),
|
|
}})
|
|
_, _ = w.Write(out) // httptest write error is not actionable in a test fake
|
|
}))
|
|
defer srv.Close()
|
|
withGraphQLURL(t, srv)
|
|
|
|
var stats []Stat
|
|
var fetchErr error
|
|
logs := captureStdout(t, func() { stats, fetchErr = fetchStats("test-token", agents) })
|
|
|
|
if fetchErr != nil {
|
|
t.Fatalf("fetchStats: %v (staleness should warn, not fail)", fetchErr)
|
|
}
|
|
if len(stats) != 3 {
|
|
t.Fatalf("expected 3 stats, got %d", len(stats))
|
|
}
|
|
|
|
if want := "::warning::repo org/stale has no push in 120 days"; !strings.Contains(logs, want) {
|
|
t.Errorf("missing stale warning %q in:\n%s", want, logs)
|
|
}
|
|
if strings.Contains(logs, "org/fresh has no push") {
|
|
t.Errorf("warned about a repo pushed 2 days ago:\n%s", logs)
|
|
}
|
|
if strings.Contains(logs, "org/stale-and-archived has no push") {
|
|
t.Errorf("stale warning duplicates the archived warning:\n%s", logs)
|
|
}
|
|
if want := "::warning::repo org/stale-and-archived is archived"; !strings.Contains(logs, want) {
|
|
t.Errorf("missing archived warning %q in:\n%s", want, logs)
|
|
}
|
|
}
|
|
|
|
func TestEnforceStarFloor(t *testing.T) {
|
|
in := []Stat{
|
|
{CanonicalKey: "a/above", Stars: minStars + 1},
|
|
{CanonicalKey: "b/exactly", Stars: minStars},
|
|
{CanonicalKey: "c/below", Stars: minStars - 1},
|
|
{CanonicalKey: "d/zero", Stars: 0},
|
|
}
|
|
|
|
got := enforceStarFloor(in)
|
|
|
|
want := []string{"a/above", "b/exactly"}
|
|
if len(got) != len(want) {
|
|
t.Fatalf("kept %d entries, want %d: %+v", len(got), len(want), got)
|
|
}
|
|
for i, w := range want {
|
|
if got[i].CanonicalKey != w {
|
|
t.Errorf("kept[%d] = %s, want %s", i, got[i].CanonicalKey, w)
|
|
}
|
|
}
|
|
}
|