test: expand compact and ui coverage
This commit is contained in:
committed by
Steve Yegge
parent
d3b6855aa9
commit
c9fa7af04c
291
internal/compact/compactor_unit_test.go
Normal file
291
internal/compact/compactor_unit_test.go
Normal file
@@ -0,0 +1,291 @@
|
||||
package compact
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
)
|
||||
|
||||
type stubStore struct {
|
||||
checkEligibilityFn func(context.Context, string, int) (bool, string, error)
|
||||
getIssueFn func(context.Context, string) (*types.Issue, error)
|
||||
updateIssueFn func(context.Context, string, map[string]interface{}, string) error
|
||||
applyCompactionFn func(context.Context, string, int, int, int, string) error
|
||||
addCommentFn func(context.Context, string, string, string) error
|
||||
markDirtyFn func(context.Context, string) error
|
||||
}
|
||||
|
||||
func (s *stubStore) CheckEligibility(ctx context.Context, issueID string, tier int) (bool, string, error) {
|
||||
if s.checkEligibilityFn != nil {
|
||||
return s.checkEligibilityFn(ctx, issueID, tier)
|
||||
}
|
||||
return false, "", nil
|
||||
}
|
||||
|
||||
func (s *stubStore) GetIssue(ctx context.Context, issueID string) (*types.Issue, error) {
|
||||
if s.getIssueFn != nil {
|
||||
return s.getIssueFn(ctx, issueID)
|
||||
}
|
||||
return nil, fmt.Errorf("GetIssue not stubbed")
|
||||
}
|
||||
|
||||
func (s *stubStore) UpdateIssue(ctx context.Context, issueID string, updates map[string]interface{}, actor string) error {
|
||||
if s.updateIssueFn != nil {
|
||||
return s.updateIssueFn(ctx, issueID, updates, actor)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stubStore) ApplyCompaction(ctx context.Context, issueID string, tier int, originalSize int, compactedSize int, commitHash string) error {
|
||||
if s.applyCompactionFn != nil {
|
||||
return s.applyCompactionFn(ctx, issueID, tier, originalSize, compactedSize, commitHash)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stubStore) AddComment(ctx context.Context, issueID, actor, comment string) error {
|
||||
if s.addCommentFn != nil {
|
||||
return s.addCommentFn(ctx, issueID, actor, comment)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stubStore) MarkIssueDirty(ctx context.Context, issueID string) error {
|
||||
if s.markDirtyFn != nil {
|
||||
return s.markDirtyFn(ctx, issueID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type stubSummarizer struct {
|
||||
summary string
|
||||
err error
|
||||
calls int
|
||||
}
|
||||
|
||||
func (s *stubSummarizer) SummarizeTier1(ctx context.Context, issue *types.Issue) (string, error) {
|
||||
s.calls++
|
||||
return s.summary, s.err
|
||||
}
|
||||
|
||||
func stubIssue() *types.Issue {
|
||||
return &types.Issue{
|
||||
ID: "bd-123",
|
||||
Title: "Fix login",
|
||||
Description: strings.Repeat("A", 20),
|
||||
Design: strings.Repeat("B", 10),
|
||||
Notes: strings.Repeat("C", 5),
|
||||
AcceptanceCriteria: "done",
|
||||
Status: types.StatusClosed,
|
||||
}
|
||||
}
|
||||
|
||||
func withGitHash(t *testing.T, hash string) func() {
|
||||
orig := gitExec
|
||||
gitExec = func(string, ...string) ([]byte, error) {
|
||||
return []byte(hash), nil
|
||||
}
|
||||
return func() { gitExec = orig }
|
||||
}
|
||||
|
||||
func TestCompactTier1_Success(t *testing.T) {
|
||||
cleanup := withGitHash(t, "deadbeef\n")
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
updateCalled := false
|
||||
applyCalled := false
|
||||
markCalled := false
|
||||
store := &stubStore{
|
||||
checkEligibilityFn: func(context.Context, string, int) (bool, string, error) { return true, "", nil },
|
||||
getIssueFn: func(context.Context, string) (*types.Issue, error) { return stubIssue(), nil },
|
||||
updateIssueFn: func(ctx context.Context, id string, updates map[string]interface{}, actor string) error {
|
||||
updateCalled = true
|
||||
if updates["description"].(string) != "short" {
|
||||
t.Fatalf("expected summarized description")
|
||||
}
|
||||
if updates["design"].(string) != "" {
|
||||
t.Fatalf("design should be cleared")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
applyCompactionFn: func(ctx context.Context, id string, tier, original, compacted int, hash string) error {
|
||||
applyCalled = true
|
||||
if hash != "deadbeef" {
|
||||
t.Fatalf("unexpected hash %q", hash)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
addCommentFn: func(ctx context.Context, id, actor, comment string) error {
|
||||
if !strings.Contains(comment, "saved") {
|
||||
t.Fatalf("unexpected comment %q", comment)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
markDirtyFn: func(context.Context, string) error {
|
||||
markCalled = true
|
||||
return nil
|
||||
},
|
||||
}
|
||||
summary := &stubSummarizer{summary: "short"}
|
||||
c := &Compactor{store: store, summarizer: summary, config: &Config{}}
|
||||
|
||||
if err := c.CompactTier1(context.Background(), "bd-123"); err != nil {
|
||||
t.Fatalf("CompactTier1 unexpected error: %v", err)
|
||||
}
|
||||
if summary.calls != 1 {
|
||||
t.Fatalf("expected summarizer used once, got %d", summary.calls)
|
||||
}
|
||||
if !updateCalled || !applyCalled || !markCalled {
|
||||
t.Fatalf("expected update/apply/mark to be called")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompactTier1_DryRun(t *testing.T) {
|
||||
store := &stubStore{
|
||||
checkEligibilityFn: func(context.Context, string, int) (bool, string, error) { return true, "", nil },
|
||||
getIssueFn: func(context.Context, string) (*types.Issue, error) { return stubIssue(), nil },
|
||||
}
|
||||
summary := &stubSummarizer{summary: "short"}
|
||||
c := &Compactor{store: store, summarizer: summary, config: &Config{DryRun: true}}
|
||||
|
||||
err := c.CompactTier1(context.Background(), "bd-123")
|
||||
if err == nil || !strings.Contains(err.Error(), "dry-run") {
|
||||
t.Fatalf("expected dry-run error, got %v", err)
|
||||
}
|
||||
if summary.calls != 0 {
|
||||
t.Fatalf("summarizer should not be used in dry run")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompactTier1_Ineligible(t *testing.T) {
|
||||
store := &stubStore{
|
||||
checkEligibilityFn: func(context.Context, string, int) (bool, string, error) { return false, "recently compacted", nil },
|
||||
}
|
||||
c := &Compactor{store: store, config: &Config{}}
|
||||
|
||||
err := c.CompactTier1(context.Background(), "bd-123")
|
||||
if err == nil || !strings.Contains(err.Error(), "recently compacted") {
|
||||
t.Fatalf("expected ineligible error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompactTier1_SummaryNotSmaller(t *testing.T) {
|
||||
commentCalled := false
|
||||
store := &stubStore{
|
||||
checkEligibilityFn: func(context.Context, string, int) (bool, string, error) { return true, "", nil },
|
||||
getIssueFn: func(context.Context, string) (*types.Issue, error) { return stubIssue(), nil },
|
||||
addCommentFn: func(ctx context.Context, id, actor, comment string) error {
|
||||
commentCalled = true
|
||||
if !strings.Contains(comment, "Tier 1 compaction skipped") {
|
||||
t.Fatalf("unexpected comment %q", comment)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
summary := &stubSummarizer{summary: strings.Repeat("X", 40)}
|
||||
c := &Compactor{store: store, summarizer: summary, config: &Config{}}
|
||||
|
||||
err := c.CompactTier1(context.Background(), "bd-123")
|
||||
if err == nil || !strings.Contains(err.Error(), "compaction would increase size") {
|
||||
t.Fatalf("expected size error, got %v", err)
|
||||
}
|
||||
if !commentCalled {
|
||||
t.Fatalf("expected warning comment to be recorded")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompactTier1_UpdateError(t *testing.T) {
|
||||
store := &stubStore{
|
||||
checkEligibilityFn: func(context.Context, string, int) (bool, string, error) { return true, "", nil },
|
||||
getIssueFn: func(context.Context, string) (*types.Issue, error) { return stubIssue(), nil },
|
||||
updateIssueFn: func(context.Context, string, map[string]interface{}, string) error { return errors.New("boom") },
|
||||
}
|
||||
summary := &stubSummarizer{summary: "short"}
|
||||
c := &Compactor{store: store, summarizer: summary, config: &Config{}}
|
||||
|
||||
err := c.CompactTier1(context.Background(), "bd-123")
|
||||
if err == nil || !strings.Contains(err.Error(), "failed to update issue") {
|
||||
t.Fatalf("expected update error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompactTier1Batch_MixedResults(t *testing.T) {
|
||||
cleanup := withGitHash(t, "cafebabe\n")
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
var mu sync.Mutex
|
||||
updated := make(map[string]int)
|
||||
applied := make(map[string]int)
|
||||
marked := make(map[string]int)
|
||||
store := &stubStore{
|
||||
checkEligibilityFn: func(ctx context.Context, id string, tier int) (bool, string, error) {
|
||||
switch id {
|
||||
case "bd-1":
|
||||
return true, "", nil
|
||||
case "bd-2":
|
||||
return false, "not eligible", nil
|
||||
default:
|
||||
return false, "", fmt.Errorf("unexpected id %s", id)
|
||||
}
|
||||
},
|
||||
getIssueFn: func(ctx context.Context, id string) (*types.Issue, error) {
|
||||
issue := stubIssue()
|
||||
issue.ID = id
|
||||
return issue, nil
|
||||
},
|
||||
updateIssueFn: func(ctx context.Context, id string, updates map[string]interface{}, actor string) error {
|
||||
mu.Lock()
|
||||
updated[id]++
|
||||
mu.Unlock()
|
||||
return nil
|
||||
},
|
||||
applyCompactionFn: func(ctx context.Context, id string, tier, original, compacted int, hash string) error {
|
||||
mu.Lock()
|
||||
applied[id]++
|
||||
mu.Unlock()
|
||||
return nil
|
||||
},
|
||||
addCommentFn: func(context.Context, string, string, string) error { return nil },
|
||||
markDirtyFn: func(ctx context.Context, id string) error {
|
||||
mu.Lock()
|
||||
marked[id]++
|
||||
mu.Unlock()
|
||||
return nil
|
||||
},
|
||||
}
|
||||
summary := &stubSummarizer{summary: "short"}
|
||||
c := &Compactor{store: store, summarizer: summary, config: &Config{Concurrency: 2}}
|
||||
|
||||
results, err := c.CompactTier1Batch(context.Background(), []string{"bd-1", "bd-2"})
|
||||
if err != nil {
|
||||
t.Fatalf("CompactTier1Batch unexpected error: %v", err)
|
||||
}
|
||||
if len(results) != 2 {
|
||||
t.Fatalf("expected 2 results, got %d", len(results))
|
||||
}
|
||||
resMap := map[string]*Result{}
|
||||
for _, r := range results {
|
||||
resMap[r.IssueID] = r
|
||||
}
|
||||
|
||||
if res := resMap["bd-1"]; res == nil || res.Err != nil || res.CompactedSize == 0 {
|
||||
t.Fatalf("expected success result for bd-1, got %+v", res)
|
||||
}
|
||||
if res := resMap["bd-2"]; res == nil || res.Err == nil || !strings.Contains(res.Err.Error(), "not eligible") {
|
||||
t.Fatalf("expected ineligible error for bd-2, got %+v", res)
|
||||
}
|
||||
if updated["bd-1"] != 1 || applied["bd-1"] != 1 || marked["bd-1"] != 1 {
|
||||
t.Fatalf("expected store operations for bd-1 exactly once")
|
||||
}
|
||||
if updated["bd-2"] != 0 || applied["bd-2"] != 0 {
|
||||
t.Fatalf("bd-2 should not be processed")
|
||||
}
|
||||
if summary.calls != 1 {
|
||||
t.Fatalf("summarizer should run once; got %d", summary.calls)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user