Skip to content

Commit be980f3

Browse files
authored
Merge pull request #66 from codeGROOVE-dev/reliable
Add support for DMs
2 parents 748b787 + fccd1f2 commit be980f3

24 files changed

+1810
-98
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
- name: Set up Go
1919
uses: actions/setup-go@v5
2020
with:
21-
go-version: '1.23'
21+
go-version: 'stable'
2222
cache: true
2323

2424
- name: Install dependencies

Makefile

Lines changed: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,35 +16,7 @@ build-registrar:
1616

1717
# Run tests with race detection and coverage
1818
test:
19-
@echo "Running tests with race detection and coverage..."
20-
@go test -v -race -coverprofile=coverage.out -covermode=atomic ./...
21-
@echo ""
22-
@echo "Coverage by package:"
23-
@go test -coverprofile=coverage.out -covermode=atomic ./... 2>&1 | grep -E "coverage:" | awk '{print $$2 "\t" $$5}' | column -t
24-
@echo ""
25-
@echo "Checking for packages below 80% coverage..."
26-
@failed=0; \
27-
packages=$$(go list ./... | grep -v "/cmd/"); \
28-
for pkg in $$packages; do \
29-
output=$$(go test -coverprofile=/dev/null "$$pkg" 2>&1); \
30-
if echo "$$output" | grep -q "\[no test files\]"; then \
31-
continue; \
32-
fi; \
33-
coverage=$$(echo "$$output" | grep "coverage:" | awk '{print $$5}' | sed 's/%//'); \
34-
if [ -n "$$coverage" ] && [ "$$coverage" != "statements" ]; then \
35-
pkg_short=$$(echo "$$pkg" | sed 's|github.com/codeGROOVE-dev/slacker/||'); \
36-
if [ "$$(echo "$$coverage < 80.0" | bc -l 2>/dev/null || echo 0)" -eq 1 ]; then \
37-
echo "❌ FAIL: $$pkg_short has $$coverage% coverage (minimum: 80%)"; \
38-
failed=1; \
39-
fi; \
40-
fi; \
41-
done; \
42-
if [ $$failed -eq 1 ]; then \
43-
echo ""; \
44-
echo "Coverage check failed. All packages must have at least 80% coverage."; \
45-
exit 1; \
46-
fi
47-
@echo "✅ All packages meet 80% coverage threshold"
19+
go test -v -race -cover ./...
4820

4921
# Format code
5022
fmt:

cmd/server/main.go

Lines changed: 3 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -162,24 +162,7 @@ func run(ctx context.Context, cancel context.CancelFunc, cfg *config.ServerConfi
162162
slackManager := slack.NewManager(cfg.SlackSigningSecret)
163163

164164
// Initialize state store (in-memory + Datastore or JSON for persistence).
165-
//nolint:interfacebloat // Interface mirrors state.Store for local type safety
166-
var stateStore interface {
167-
Thread(owner, repo string, number int, channelID string) (state.ThreadInfo, bool)
168-
SaveThread(owner, repo string, number int, channelID string, info state.ThreadInfo) error
169-
LastDM(userID, prURL string) (time.Time, bool)
170-
RecordDM(userID, prURL string, sentAt time.Time) error
171-
DMMessage(userID, prURL string) (state.DMInfo, bool)
172-
SaveDMMessage(userID, prURL string, info state.DMInfo) error
173-
ListDMUsers(prURL string) []string
174-
LastDigest(userID, date string) (time.Time, bool)
175-
RecordDigest(userID, date string, sentAt time.Time) error
176-
WasProcessed(eventKey string) bool
177-
MarkProcessed(eventKey string, ttl time.Duration) error
178-
LastNotification(prURL string) time.Time
179-
RecordNotification(prURL string, notifiedAt time.Time) error
180-
Cleanup() error
181-
Close() error
182-
}
165+
var stateStore state.Store
183166

184167
// Check if Datastore should be used via DATASTORE=<database-id>
185168
// Examples:
@@ -256,7 +239,7 @@ func run(ctx context.Context, cancel context.CancelFunc, cfg *config.ServerConfi
256239
slog.Info("configured Slack manager with state store for DM tracking")
257240

258241
// Initialize notification manager for multi-workspace notifications.
259-
notifier := notify.New(notify.WrapSlackManager(slackManager), configManager)
242+
notifier := notify.New(notify.WrapSlackManager(slackManager), configManager, stateStore)
260243

261244
// Initialize event router for multi-workspace event handling.
262245
eventRouter := slack.NewEventRouter(slackManager)
@@ -679,23 +662,7 @@ func runBotCoordinators(
679662
githubManager *github.Manager,
680663
configManager *config.Manager,
681664
notifier *notify.Manager,
682-
stateStore interface {
683-
Thread(owner, repo string, number int, channelID string) (state.ThreadInfo, bool)
684-
SaveThread(owner, repo string, number int, channelID string, info state.ThreadInfo) error
685-
LastDM(userID, prURL string) (time.Time, bool)
686-
RecordDM(userID, prURL string, sentAt time.Time) error
687-
DMMessage(userID, prURL string) (state.DMInfo, bool)
688-
SaveDMMessage(userID, prURL string, info state.DMInfo) error
689-
ListDMUsers(prURL string) []string
690-
LastDigest(userID, date string) (time.Time, bool)
691-
RecordDigest(userID, date string, sentAt time.Time) error
692-
WasProcessed(eventKey string) bool
693-
MarkProcessed(eventKey string, ttl time.Duration) error
694-
LastNotification(prURL string) time.Time
695-
RecordNotification(prURL string, notifiedAt time.Time) error
696-
Cleanup() error
697-
Close() error
698-
},
665+
stateStore state.Store,
699666
sprinklerURL string,
700667
) error {
701668
cm := &coordinatorManager{

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ require (
2626
github.com/cloudflare/circl v1.6.1 // indirect
2727
github.com/google/go-cmp v0.7.0 // indirect
2828
github.com/google/go-querystring v1.1.0 // indirect
29+
github.com/google/uuid v1.6.0 // indirect
2930
github.com/gorilla/websocket v1.5.3 // indirect
3031
github.com/kr/pretty v0.3.1 // indirect
3132
github.com/rogpeppe/go-internal v1.14.1 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ github.com/google/go-github/v50 v50.2.0 h1:j2FyongEHlO9nxXLc+LP3wuBSVU9mVxfpdYUe
3030
github.com/google/go-github/v50 v50.2.0/go.mod h1:VBY8FB6yPIjrtKhozXv4FQupxKLS6H4m6xFZlT43q8Q=
3131
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
3232
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
33+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
34+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
3335
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
3436
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
3537
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=

pkg/bot/integration_test.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/codeGROOVE-dev/slacker/pkg/notify"
1111
"github.com/codeGROOVE-dev/slacker/pkg/slack"
1212
"github.com/codeGROOVE-dev/slacker/pkg/slacktest"
13+
"github.com/codeGROOVE-dev/slacker/pkg/state"
1314
"github.com/codeGROOVE-dev/slacker/pkg/usermapping"
1415
"github.com/codeGROOVE-dev/turnclient/pkg/turn"
1516
slackapi "github.com/slack-go/slack"
@@ -231,7 +232,10 @@ func TestDMDelayLogicIntegration(t *testing.T) {
231232
dmDelay: 65, // 65 minute delay
232233
}
233234

234-
notifier := notify.New(notify.WrapSlackManager(slackManager), configMgr)
235+
// Create in-memory store for pending DMs
236+
store := state.NewMemoryStore()
237+
238+
notifier := notify.New(notify.WrapSlackManager(slackManager), configMgr, store)
235239

236240
prInfo := notify.PRInfo{
237241
Owner: "test",
@@ -291,7 +295,8 @@ func TestDMDelayLogicIntegration(t *testing.T) {
291295
// Create fresh tracker with initialized maps
292296
notifier.Tracker = &notify.NotificationTracker{}
293297
// Initialize the tracker by creating a new notifier
294-
notifier = notify.New(notify.WrapSlackManager(slackManager), configMgr)
298+
store = state.NewMemoryStore() // Reset store as well
299+
notifier = notify.New(notify.WrapSlackManager(slackManager), configMgr, store)
295300

296301
// Setup test scenario
297302
if tt.setupFunc != nil {

pkg/config/config.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,7 @@ func (m *Manager) LoadConfig(ctx context.Context, org string) error {
374374
"team_id": config.Global.TeamID,
375375
"email_domain": config.Global.EmailDomain,
376376
"daily_reminders": config.Global.DailyReminders,
377+
"reminder_dm_delay": config.Global.ReminderDMDelay,
377378
"total_channels": len(config.Channels),
378379
"muted_channels": muted,
379380
"wildcard_channels": wildcard,
@@ -575,20 +576,36 @@ func (m *Manager) ReminderDMDelay(org, channel string) int {
575576

576577
config, exists := m.configs[org]
577578
if !exists {
579+
slog.Debug("no config for org - using default delay",
580+
logFieldOrg, org,
581+
"default_delay_mins", defaultReminderDMDelayMinutes)
578582
return defaultReminderDMDelayMinutes
579583
}
580584

581585
// Check for channel-specific override
582586
if channelConfig, ok := config.Channels[channel]; ok {
583587
if channelConfig.ReminderDMDelay != nil {
588+
slog.Debug("using channel-specific reminder delay",
589+
logFieldOrg, org,
590+
"channel", channel,
591+
"delay_mins", *channelConfig.ReminderDMDelay)
584592
return *channelConfig.ReminderDMDelay
585593
}
586594
}
587595

588596
// Return global setting (or default if not set)
589597
if config.Global.ReminderDMDelay > 0 {
598+
slog.Debug("using global reminder delay",
599+
logFieldOrg, org,
600+
"channel", channel,
601+
"delay_mins", config.Global.ReminderDMDelay)
590602
return config.Global.ReminderDMDelay
591603
}
604+
slog.Debug("global delay is 0 or unset - using default",
605+
logFieldOrg, org,
606+
"channel", channel,
607+
"global_value", config.Global.ReminderDMDelay,
608+
"default_delay_mins", defaultReminderDMDelayMinutes)
592609
return defaultReminderDMDelayMinutes
593610
}
594611

pkg/config/config_test.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1071,6 +1071,112 @@ channels: [1, 2, 3]
10711071
}
10721072
}
10731073

1074+
func TestManager_LoadConfigCodeGROOVEProdConfig(t *testing.T) {
1075+
// Test with actual production config from codeGROOVE-dev/.codeGROOVE/slack.yaml
1076+
prodYAML := `global:
1077+
team_id: T09CJ7X7T7Y
1078+
email_domain: codegroove.dev
1079+
reminder_dm_delay: 1
1080+
1081+
channels:
1082+
goose:
1083+
mute: true
1084+
1085+
all-codegroove:
1086+
repos:
1087+
- "*"
1088+
1089+
social:
1090+
repos:
1091+
- goose
1092+
- sprinkler
1093+
- slacker
1094+
`
1095+
1096+
handler := func(w http.ResponseWriter, r *http.Request) {
1097+
content := base64.StdEncoding.EncodeToString([]byte(prodYAML))
1098+
encoding := "base64"
1099+
response := github.RepositoryContent{
1100+
Type: github.String("file"),
1101+
Content: &content,
1102+
Encoding: &encoding,
1103+
}
1104+
w.Header().Set("Content-Type", "application/json")
1105+
if err := json.NewEncoder(w).Encode(response); err != nil {
1106+
t.Errorf("failed to encode response: %v", err)
1107+
}
1108+
}
1109+
1110+
client, server := createTestGitHubClient(handler)
1111+
defer server.Close()
1112+
1113+
m := New()
1114+
m.SetGitHubClient("codeGROOVE-dev", client)
1115+
1116+
ctx := context.Background()
1117+
err := m.LoadConfig(ctx, "codeGROOVE-dev")
1118+
if err != nil {
1119+
t.Fatalf("unexpected error loading production config: %v", err)
1120+
}
1121+
1122+
// Verify config was loaded correctly
1123+
cfg, exists := m.Config("codeGROOVE-dev")
1124+
if !exists {
1125+
t.Fatal("expected config to exist after loading")
1126+
}
1127+
if cfg.Global.TeamID != "T09CJ7X7T7Y" {
1128+
t.Errorf("expected TeamID T09CJ7X7T7Y, got %q", cfg.Global.TeamID)
1129+
}
1130+
if cfg.Global.EmailDomain != "codegroove.dev" {
1131+
t.Errorf("expected email domain codegroove.dev, got %q", cfg.Global.EmailDomain)
1132+
}
1133+
if cfg.Global.ReminderDMDelay != 1 {
1134+
t.Errorf("expected reminder delay 1 minute, got %d", cfg.Global.ReminderDMDelay)
1135+
}
1136+
if len(cfg.Channels) != 3 {
1137+
t.Errorf("expected 3 channels, got %d", len(cfg.Channels))
1138+
}
1139+
1140+
// Verify goose channel is muted
1141+
gooseChannel, exists := cfg.Channels["goose"]
1142+
if !exists {
1143+
t.Error("expected goose channel to exist")
1144+
}
1145+
if !gooseChannel.Mute {
1146+
t.Error("expected goose channel to be muted")
1147+
}
1148+
1149+
// Verify all-codegroove has wildcard
1150+
allChannel, exists := cfg.Channels["all-codegroove"]
1151+
if !exists {
1152+
t.Error("expected all-codegroove channel to exist")
1153+
}
1154+
if len(allChannel.Repos) != 1 || allChannel.Repos[0] != "*" {
1155+
t.Errorf("expected all-codegroove to have wildcard repo, got %v", allChannel.Repos)
1156+
}
1157+
1158+
// Verify social channel repos
1159+
socialChannel, exists := cfg.Channels["social"]
1160+
if !exists {
1161+
t.Error("expected social channel to exist")
1162+
}
1163+
expectedRepos := []string{"goose", "sprinkler", "slacker"}
1164+
if len(socialChannel.Repos) != len(expectedRepos) {
1165+
t.Errorf("expected %d repos in social channel, got %d", len(expectedRepos), len(socialChannel.Repos))
1166+
}
1167+
for i, repo := range expectedRepos {
1168+
if i >= len(socialChannel.Repos) || socialChannel.Repos[i] != repo {
1169+
t.Errorf("expected repo %q at index %d in social channel, got %v", repo, i, socialChannel.Repos)
1170+
}
1171+
}
1172+
1173+
// Verify ReminderDMDelay returns correct value
1174+
delay := m.ReminderDMDelay("codeGROOVE-dev", "social")
1175+
if delay != 1 {
1176+
t.Errorf("expected ReminderDMDelay to return 1 minute, got %d", delay)
1177+
}
1178+
}
1179+
10741180
func TestManager_LoadConfigEmptyContent(t *testing.T) {
10751181
handler := func(w http.ResponseWriter, r *http.Request) {
10761182
// Return a response with nil Content

pkg/home/fetcher_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,18 @@ func (m *mockStateStore) Cleanup() error {
260260
return nil
261261
}
262262

263+
func (m *mockStateStore) QueuePendingDM(dm state.PendingDM) error {
264+
return nil
265+
}
266+
267+
func (m *mockStateStore) GetPendingDMs(before time.Time) ([]state.PendingDM, error) {
268+
return nil, nil
269+
}
270+
271+
func (m *mockStateStore) RemovePendingDM(id string) error {
272+
return nil
273+
}
274+
263275
func (m *mockStateStore) Close() error {
264276
return nil
265277
}

pkg/notify/daily_digest_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -603,7 +603,7 @@ func TestNewDailyDigestScheduler_FactoryWorks(t *testing.T) {
603603
mockConfigMgr := &mockConfigProvider{}
604604
mockState := &mockStateProvider{}
605605
mockSlack := &mockSlackManagerWithClient{}
606-
manager := New(mockSlack, mockConfigMgr)
606+
manager := New(mockSlack, mockConfigMgr, &mockStore{})
607607

608608
scheduler := NewDailyDigestScheduler(manager, mockGitHubMgr, mockConfigMgr, mockState, mockSlack)
609609

0 commit comments

Comments
 (0)