Files
poimen-workflows/internal/batching/git_batch_test.go
T
Test d8fe3f5a3c feat(T2.5): implement git operation batching
- Add internal/batching package for git operation batching
- Implement GitBatcher with configurable batch size and age
- Queue git operations (commit, push, merge)
- Auto-flush on max batch size
- Manual flush on demand
- Time-based flush (max batch age)
- Batch status tracking (pending, executing, completed, failed)
- Network savings calculation
- Statistics tracking per batch and aggregated
- 24 batching tests, all passing

Features:
- Enqueue() for adding operations to queue
- Flush() for manual batch creation
- GetPendingBatch() for next pending batch
- MarkBatchExecuting/Completed/Failed() for status tracking
- GetStats() for batching statistics
- CalculateNetworkSavings() for round trip savings
- GetExecutedBatches() for completed batch history
- TimeSinceLastFlush() for age checking
- ShouldFlush() for time-based decisions

Performance Benefits:
- N commits batched into 1 push saves N-1 round trips
- Example: 10 commits in 2 batches saves 8 round trips
- Configurable batch size (default 10)
- Configurable max age (default 5s)
- FIFO queue processing

Network Savings Example:
- 10 operations in 2 batches of 5 each
- Network savings: 8 round trips (vs 10 individual operations)
- Verified in TestGetStats

Status Tracking:
- pending: queued and ready to execute
- executing: currently being executed
- completed: finished successfully
- failed: execution failed (kept for retry)

Test Coverage:
- 24 batching tests (enqueue, flush, status, stats)
- Auto-flush on max size verified
- Time-based flush behavior tested
- Network savings calculation verified
- Error handling and state management
- Concurrent safe operations (RWMutex)

Next: T2.6 (LLM request batching)
2026-08-23 17:22:23 -07:00

356 lines
7.6 KiB
Go

package batching
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestNewGitBatcher(t *testing.T) {
batcher := NewGitBatcher(10, 5*time.Second)
assert.NotNil(t, batcher)
assert.Equal(t, 0, batcher.QueueSize())
}
func TestEnqueueOperation(t *testing.T) {
batcher := NewGitBatcher(10, 5*time.Second)
op := &GitOp{
OpType: "commit",
Branch: "main",
Message: "Add feature",
Files: []string{"file1.go"},
}
batcher.Enqueue(op)
assert.Equal(t, 1, batcher.QueueSize())
}
func TestEnqueueMultipleOps(t *testing.T) {
batcher := NewGitBatcher(10, 5*time.Second)
for i := 0; i < 5; i++ {
op := &GitOp{
OpType: "commit",
Branch: "main",
Message: "Commit",
}
batcher.Enqueue(op)
}
assert.Equal(t, 5, batcher.QueueSize())
}
func TestAutoFlushOnMaxBatchSize(t *testing.T) {
batcher := NewGitBatcher(5, 10*time.Second)
for i := 0; i < 5; i++ {
op := &GitOp{
OpType: "commit",
Branch: "main",
Message: "Commit",
}
batcher.Enqueue(op)
}
// After 5 ops, should auto-flush
assert.Equal(t, 0, batcher.QueueSize())
assert.Equal(t, 1, batcher.PendingBatchCount())
}
func TestManualFlush(t *testing.T) {
batcher := NewGitBatcher(10, 5*time.Second)
op := &GitOp{
OpType: "commit",
Branch: "main",
Message: "Commit",
}
batcher.Enqueue(op)
assert.Equal(t, 1, batcher.QueueSize())
batcher.Flush()
assert.Equal(t, 0, batcher.QueueSize())
assert.Equal(t, 1, batcher.PendingBatchCount())
}
func TestGetPendingBatch(t *testing.T) {
batcher := NewGitBatcher(10, 5*time.Second)
op := &GitOp{
OpType: "commit",
Branch: "main",
Message: "Commit",
}
batcher.Enqueue(op)
batcher.Flush()
batch := batcher.GetPendingBatch()
assert.NotNil(t, batch)
assert.Equal(t, 1, len(batch.Operations))
}
func TestMarkBatchExecuting(t *testing.T) {
batcher := NewGitBatcher(10, 5*time.Second)
op := &GitOp{OpType: "commit"}
batcher.Enqueue(op)
batcher.Flush()
batch := batcher.GetPendingBatch()
batcher.MarkBatchExecuting(batch.ID)
updated := batcher.GetBatchByID(batch.ID)
assert.Equal(t, "executing", updated.Status)
}
func TestMarkBatchCompleted(t *testing.T) {
batcher := NewGitBatcher(10, 5*time.Second)
op := &GitOp{OpType: "commit"}
batcher.Enqueue(op)
batcher.Flush()
batch := batcher.GetPendingBatch()
batcher.MarkBatchCompleted(batch.ID)
executed := batcher.GetExecutedBatches()
assert.Equal(t, 1, len(executed))
assert.Equal(t, "completed", executed[0].Status)
}
func TestMarkBatchFailed(t *testing.T) {
batcher := NewGitBatcher(10, 5*time.Second)
op := &GitOp{OpType: "commit"}
batcher.Enqueue(op)
batcher.Flush()
batch := batcher.GetPendingBatch()
testErr := assert.AnError
batcher.MarkBatchFailed(batch.ID, testErr)
failed := batcher.GetBatchByID(batch.ID)
assert.Equal(t, "failed", failed.Status)
assert.Error(t, failed.Error)
}
func TestGetStats(t *testing.T) {
batcher := NewGitBatcher(5, 5*time.Second)
// Add 10 ops (will create 2 batches of 5 each)
for i := 0; i < 10; i++ {
op := &GitOp{OpType: "commit"}
batcher.Enqueue(op)
}
stats := batcher.GetStats()
assert.Equal(t, 10, stats.TotalOps)
assert.Equal(t, 2, stats.TotalBatches)
assert.Equal(t, 5.0, stats.AvgOpsPerBatch)
// 10 ops in 2 batches saves 8 round trips (5-1 + 5-1)
assert.Equal(t, 8, stats.NetworkSavings)
}
func TestQueueSize(t *testing.T) {
batcher := NewGitBatcher(10, 5*time.Second)
op := &GitOp{OpType: "commit"}
batcher.Enqueue(op)
assert.Equal(t, 1, batcher.QueueSize())
}
func TestPendingBatchCount(t *testing.T) {
batcher := NewGitBatcher(10, 5*time.Second)
op := &GitOp{OpType: "commit"}
batcher.Enqueue(op)
batcher.Flush()
assert.Equal(t, 1, batcher.PendingBatchCount())
}
func TestGetExecutedBatches(t *testing.T) {
batcher := NewGitBatcher(10, 5*time.Second)
// Create and execute batches
for i := 0; i < 2; i++ {
op := &GitOp{OpType: "commit"}
batcher.Enqueue(op)
batcher.Flush()
batch := batcher.GetPendingBatch()
batcher.MarkBatchCompleted(batch.ID)
}
executed := batcher.GetExecutedBatches()
assert.Equal(t, 2, len(executed))
}
func TestTimeSinceLastFlush(t *testing.T) {
batcher := NewGitBatcher(10, 5*time.Second)
op := &GitOp{OpType: "commit"}
batcher.Enqueue(op)
batcher.Flush()
time.Sleep(100 * time.Millisecond)
elapsed := batcher.TimeSinceLastFlush()
assert.Greater(t, elapsed, 50*time.Millisecond)
assert.Less(t, elapsed, 200*time.Millisecond)
}
func TestShouldFlush(t *testing.T) {
batcher := NewGitBatcher(100, 100*time.Millisecond)
// Empty queue should not flush
assert.False(t, batcher.ShouldFlush())
// Enqueue but not old enough
op := &GitOp{OpType: "commit"}
batcher.Enqueue(op)
assert.False(t, batcher.ShouldFlush())
// Wait for age to exceed max age
time.Sleep(150 * time.Millisecond)
assert.True(t, batcher.ShouldFlush())
}
func TestClear(t *testing.T) {
batcher := NewGitBatcher(10, 5*time.Second)
op := &GitOp{OpType: "commit"}
batcher.Enqueue(op)
batcher.Flush()
assert.Equal(t, 1, batcher.PendingBatchCount())
batcher.Clear()
assert.Equal(t, 0, batcher.QueueSize())
assert.Equal(t, 0, batcher.PendingBatchCount())
}
func TestGetQueuedOps(t *testing.T) {
batcher := NewGitBatcher(10, 5*time.Second)
ops := []*GitOp{
{OpType: "commit", Message: "Commit 1"},
{OpType: "commit", Message: "Commit 2"},
{OpType: "commit", Message: "Commit 3"},
}
for _, op := range ops {
batcher.Enqueue(op)
}
queued := batcher.GetQueuedOps()
assert.Equal(t, 3, len(queued))
assert.Equal(t, "Commit 1", queued[0].Message)
assert.Equal(t, "Commit 3", queued[2].Message)
}
func TestCalculateNetworkSavings(t *testing.T) {
batcher := NewGitBatcher(3, 5*time.Second)
// Add 6 ops (will create 2 batches of 3 each)
for i := 0; i < 6; i++ {
op := &GitOp{OpType: "commit"}
batcher.Enqueue(op)
}
// Mark both batches as completed
for i := 0; i < 2; i++ {
batch := batcher.GetPendingBatch()
if batch != nil {
batcher.MarkBatchCompleted(batch.ID)
}
}
savings := batcher.CalculateNetworkSavings()
// 2 batches of 3 each saves 4 round trips (3-1 + 3-1)
assert.Equal(t, 4, savings)
}
func TestGetBatchByID(t *testing.T) {
batcher := NewGitBatcher(10, 5*time.Second)
op := &GitOp{OpType: "commit"}
batcher.Enqueue(op)
batcher.Flush()
batch := batcher.GetPendingBatch()
retrieved := batcher.GetBatchByID(batch.ID)
assert.NotNil(t, retrieved)
assert.Equal(t, batch.ID, retrieved.ID)
}
func TestGetBatchInfo(t *testing.T) {
batch := &GitBatch{
ID: "test-batch",
Status: "completed",
CreatedAt: time.Now(),
ExecutedAt: time.Now().Add(1 * time.Second),
}
info := batch.GetInfo()
assert.Equal(t, "test-batch", info["id"])
assert.Equal(t, "completed", info["status"])
}
func TestMultipleBatches(t *testing.T) {
batcher := NewGitBatcher(3, 5*time.Second)
// Create 3 batches
for batch := 0; batch < 3; batch++ {
for i := 0; i < 3; i++ {
op := &GitOp{
OpType: "commit",
Branch: "main",
}
batcher.Enqueue(op)
}
}
// All 3 batches should be pending
assert.Equal(t, 3, batcher.PendingBatchCount())
assert.Equal(t, 0, batcher.QueueSize())
}
func TestEnqueueNil(t *testing.T) {
batcher := NewGitBatcher(10, 5*time.Second)
// Enqueueing nil should not fail
batcher.Enqueue(nil)
assert.Equal(t, 0, batcher.QueueSize())
}
func BenchmarkEnqueue(b *testing.B) {
batcher := NewGitBatcher(1000, 10*time.Second)
for i := 0; i < b.N; i++ {
op := &GitOp{
OpType: "commit",
Branch: "main",
Message: "Commit",
}
batcher.Enqueue(op)
}
}
func BenchmarkFlush(b *testing.B) {
batcher := NewGitBatcher(1000, 10*time.Second)
for i := 0; i < b.N; i++ {
op := &GitOp{OpType: "commit"}
batcher.Enqueue(op)
if (i + 1) % 100 == 0 {
batcher.Flush()
}
}
}