Skip to content

Commit 16392cc

Browse files
authored
Merge pull request #118 from tstromberg/others
change dashboard URL to my.reviewGOOSE.dev & add test coverage
2 parents 760c328 + 19005b0 commit 16392cc

23 files changed

+1953
-281
lines changed

cmd/reviewGOOSE/cache.go

Lines changed: 53 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -2,101 +2,79 @@ package main
22

33
import (
44
"context"
5-
"crypto/sha256"
6-
"encoding/hex"
75
"encoding/json"
86
"fmt"
97
"log/slog"
10-
"os"
11-
"path/filepath"
12-
"strings"
138
"time"
149

10+
"github.com/codeGROOVE-dev/goose/pkg/prcache"
1511
"github.com/codeGROOVE-dev/goose/pkg/safebrowse"
1612
"github.com/codeGROOVE-dev/retry"
1713
"github.com/codeGROOVE-dev/turnclient/pkg/turn"
1814
)
1915

20-
type cacheEntry struct {
21-
Data *turn.CheckResponse `json:"data"`
22-
CachedAt time.Time `json:"cached_at"`
23-
UpdatedAt time.Time `json:"updated_at"`
24-
}
25-
2616
// checkCache checks the cache for a PR and returns the cached data if valid.
2717
// Returns (data, hit, running) where running indicates incomplete tests.
28-
func (app *App) checkCache(path, url string, updatedAt time.Time) (data *turn.CheckResponse, hit, running bool) {
29-
b, err := os.ReadFile(path)
30-
if err != nil {
31-
if !os.IsNotExist(err) {
32-
slog.Debug("[CACHE] Cache file read error", "url", url, "error", err)
18+
func (app *App) checkCache(cacheManager *prcache.Manager, path, url string, updatedAt time.Time) (data *turn.CheckResponse, hit, running bool) {
19+
// State check function for incomplete tests
20+
stateCheck := func(d any) bool {
21+
if m, ok := d.(map[string]any); ok {
22+
if pr, ok := m["pull_request"].(map[string]any); ok {
23+
if state, ok := pr["test_state"].(string); ok {
24+
incomplete := state == "running" || state == "queued" || state == "pending"
25+
// Only bypass for recently updated PRs
26+
if incomplete && time.Since(updatedAt) < time.Hour {
27+
return true
28+
}
29+
}
30+
}
3331
}
34-
return nil, false, false
32+
return false
3533
}
3634

37-
var e cacheEntry
38-
if err := json.Unmarshal(b, &e); err != nil {
39-
slog.Warn("Failed to unmarshal cache data", "url", url, "error", err)
40-
if err := os.Remove(path); err != nil {
41-
slog.Debug("Failed to remove corrupted cache file", "error", err)
42-
}
35+
// Determine TTL based on whether we expect tests to be running
36+
ttl := cacheTTL
37+
bypassTTL := runningTestsCacheBypass
38+
39+
result, err := cacheManager.Get(path, updatedAt, ttl, bypassTTL, stateCheck)
40+
if err != nil {
41+
slog.Debug("[CACHE] Cache error", "url", url, "error", err)
4342
return nil, false, false
4443
}
45-
if e.Data == nil {
46-
slog.Warn("Cache entry missing data", "url", url)
47-
if err := os.Remove(path); err != nil {
48-
slog.Debug("Failed to remove corrupted cache file", "error", err)
49-
}
50-
return nil, false, false
44+
45+
if !result.Hit {
46+
return nil, false, result.ShouldBypass
5147
}
5248

53-
// Determine TTL based on test state - use shorter TTL for incomplete tests
54-
state := e.Data.PullRequest.TestState
55-
incomplete := state == "running" || state == "queued" || state == "pending"
56-
ttl := cacheTTL
57-
if incomplete {
58-
ttl = runningTestsCacheTTL
49+
// Extract turn.CheckResponse from cached data
50+
if result.Entry == nil || result.Entry.Data == nil {
51+
return nil, false, false
5952
}
6053

61-
// Check if cache is expired or PR updated
62-
if time.Since(e.CachedAt) >= ttl || !e.UpdatedAt.Equal(updatedAt) {
63-
if !e.UpdatedAt.Equal(updatedAt) {
64-
slog.Debug("[CACHE] Cache miss - PR updated",
65-
"url", url,
66-
"cached_pr_time", e.UpdatedAt.Format(time.RFC3339),
67-
"current_pr_time", updatedAt.Format(time.RFC3339))
68-
} else {
69-
slog.Debug("[CACHE] Cache miss - TTL expired",
70-
"url", url,
71-
"cached_at", e.CachedAt.Format(time.RFC3339),
72-
"cache_age", time.Since(e.CachedAt).Round(time.Second),
73-
"ttl", ttl,
74-
"test_state", state)
75-
}
76-
return nil, false, incomplete
54+
// Convert map back to CheckResponse
55+
dataBytes, err := json.Marshal(result.Entry.Data)
56+
if err != nil {
57+
slog.Warn("Failed to marshal cached data", "url", url, "error", err)
58+
return nil, false, false
7759
}
7860

79-
// Invalidate cache for incomplete tests on recently-updated PRs to catch completion
80-
// Skip this for PRs not updated in over an hour - their pending tests are likely stale
81-
age := time.Since(e.CachedAt)
82-
if incomplete && age < runningTestsCacheBypass && time.Since(updatedAt) < time.Hour {
83-
slog.Debug("[CACHE] Cache invalidated - tests incomplete and cache entry is fresh",
84-
"url", url,
85-
"test_state", state,
86-
"cache_age", age.Round(time.Minute),
87-
"cached_at", e.CachedAt.Format(time.RFC3339))
88-
return nil, false, true
61+
var response turn.CheckResponse
62+
if err := json.Unmarshal(dataBytes, &response); err != nil {
63+
slog.Warn("Failed to unmarshal cached data", "url", url, "error", err)
64+
return nil, false, false
8965
}
9066

9167
slog.Debug("[CACHE] Cache hit",
9268
"url", url,
93-
"cached_at", e.CachedAt.Format(time.RFC3339),
94-
"cache_age", time.Since(e.CachedAt).Round(time.Second),
95-
"pr_updated_at", e.UpdatedAt.Format(time.RFC3339))
69+
"cached_at", result.Entry.CachedAt.Format(time.RFC3339),
70+
"cache_age", time.Since(result.Entry.CachedAt).Round(time.Second),
71+
"pr_updated_at", result.Entry.UpdatedAt.Format(time.RFC3339))
72+
9673
if app.healthMonitor != nil {
9774
app.healthMonitor.recordCacheAccess(true)
9875
}
99-
return e.Data, true, false
76+
77+
return &response, true, false
10078
}
10179

10280
// turnData fetches Turn API data with caching.
@@ -110,21 +88,20 @@ func (app *App) turnData(ctx context.Context, url string, updatedAt time.Time) (
11088
return nil, false, fmt.Errorf("invalid URL: %w", err)
11189
}
11290

113-
// Create cache key from URL and updated timestamp
114-
key := fmt.Sprintf("%s-%s", url, updatedAt.Format(time.RFC3339))
115-
h := sha256.Sum256([]byte(key))
116-
path := filepath.Join(app.cacheDir, hex.EncodeToString(h[:])[:16]+".json")
91+
// Create cache manager and path
92+
cacheManager := prcache.NewManager(app.cacheDir)
93+
cacheKey := prcache.CacheKey(url, updatedAt)
94+
path := cacheManager.CachePath(cacheKey)
11795

11896
slog.Debug("[CACHE] Checking cache",
11997
"url", url,
12098
"updated_at", updatedAt.Format(time.RFC3339),
121-
"cache_key", key,
122-
"cache_file", filepath.Base(path))
99+
"cache_key", cacheKey)
123100

124101
// Check cache unless --no-cache flag is set
125102
var running bool
126103
if !app.noCache {
127-
data, hit, r := app.checkCache(path, url, updatedAt)
104+
data, hit, r := app.checkCache(cacheManager, path, url, updatedAt)
128105
if hit {
129106
return data, true, nil
130107
}
@@ -203,18 +180,11 @@ func (app *App) turnData(ctx context.Context, url string, updatedAt time.Time) (
203180

204181
// Save to cache (don't fail if caching fails)
205182
if !app.noCache && data != nil {
206-
e := cacheEntry{Data: data, CachedAt: time.Now(), UpdatedAt: updatedAt}
207-
b, err := json.Marshal(e)
208-
if err != nil {
209-
slog.Error("Failed to marshal cache data", "url", url, "error", err)
210-
} else if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
211-
slog.Error("Failed to create cache directory", "error", err)
212-
} else if err := os.WriteFile(path, b, 0o600); err != nil {
213-
slog.Error("Failed to write cache file", "error", err)
183+
if err := cacheManager.Put(path, data, updatedAt); err != nil {
184+
slog.Error("Failed to save cache", "url", url, "error", err)
214185
} else {
215186
slog.Debug("[CACHE] Saved to cache",
216187
"url", url,
217-
"cache_file", filepath.Base(path),
218188
"test_state", data.PullRequest.TestState)
219189
}
220190
}
@@ -224,33 +194,8 @@ func (app *App) turnData(ctx context.Context, url string, updatedAt time.Time) (
224194

225195
// cleanupOldCache removes cache files older than the cleanup interval (15 days).
226196
func (app *App) cleanupOldCache() {
227-
entries, err := os.ReadDir(app.cacheDir)
228-
if err != nil {
229-
slog.Error("Failed to read cache directory for cleanup", "error", err)
230-
return
231-
}
232-
233-
var cleaned, errs int
234-
for _, e := range entries {
235-
if !strings.HasSuffix(e.Name(), ".json") {
236-
continue
237-
}
238-
info, err := e.Info()
239-
if err != nil {
240-
slog.Error("Failed to get file info for cache entry", "entry", e.Name(), "error", err)
241-
errs++
242-
continue
243-
}
244-
if time.Since(info.ModTime()) > cacheCleanupInterval {
245-
p := filepath.Join(app.cacheDir, e.Name())
246-
if err := os.Remove(p); err != nil {
247-
slog.Error("Failed to remove old cache file", "file", p, "error", err)
248-
errs++
249-
} else {
250-
cleaned++
251-
}
252-
}
253-
}
197+
cacheManager := prcache.NewManager(app.cacheDir)
198+
cleaned, errs := cacheManager.CleanupOldFiles(cacheCleanupInterval)
254199

255200
if cleaned > 0 || errs > 0 {
256201
slog.Info("Cache cleanup completed", "removed", cleaned, "errors", errs)

cmd/reviewGOOSE/deadlock_test.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"sync"
55
"testing"
66
"time"
7+
8+
"github.com/codeGROOVE-dev/goose/pkg/ratelimit"
79
)
810

911
// TestConcurrentMenuOperations tests that concurrent menu operations don't cause deadlocks
@@ -14,7 +16,7 @@ func TestConcurrentMenuOperations(t *testing.T) {
1416
hiddenOrgs: make(map[string]bool),
1517
seenOrgs: make(map[string]bool),
1618
blockedPRTimes: make(map[string]time.Time),
17-
browserRateLimiter: NewBrowserRateLimiter(startupGracePeriod, 5, defaultMaxBrowserOpensDay),
19+
browserRateLimiter: ratelimit.NewBrowserRateLimiter(startupGracePeriod, 5, defaultMaxBrowserOpensDay),
1820
systrayInterface: &MockSystray{},
1921
incoming: []PR{
2022
{Repository: "org1/repo1", Number: 1, Title: "Fix bug", URL: "https://github.com/org1/repo1/pull/1"},
@@ -101,7 +103,7 @@ func TestMenuClickDeadlockScenario(t *testing.T) {
101103
hiddenOrgs: make(map[string]bool),
102104
seenOrgs: make(map[string]bool),
103105
blockedPRTimes: make(map[string]time.Time),
104-
browserRateLimiter: NewBrowserRateLimiter(startupGracePeriod, 5, defaultMaxBrowserOpensDay),
106+
browserRateLimiter: ratelimit.NewBrowserRateLimiter(startupGracePeriod, 5, defaultMaxBrowserOpensDay),
105107
systrayInterface: &MockSystray{},
106108
incoming: []PR{
107109
{Repository: "org1/repo1", Number: 1, Title: "Test PR", URL: "https://github.com/org1/repo1/pull/1"},
@@ -142,7 +144,7 @@ func TestRapidMenuClicks(t *testing.T) {
142144
hiddenOrgs: make(map[string]bool),
143145
seenOrgs: make(map[string]bool),
144146
blockedPRTimes: make(map[string]time.Time),
145-
browserRateLimiter: NewBrowserRateLimiter(startupGracePeriod, 5, defaultMaxBrowserOpensDay),
147+
browserRateLimiter: ratelimit.NewBrowserRateLimiter(startupGracePeriod, 5, defaultMaxBrowserOpensDay),
146148
systrayInterface: &MockSystray{},
147149
lastSearchAttempt: time.Now().Add(-15 * time.Second), // Allow first click
148150
incoming: []PR{

0 commit comments

Comments
 (0)