Validation and testing improvements (bd-74, bd-77)
- Remove unreachable function DroppedEventsCount from RPC server - Add TestMutationToExportLatency for event-driven daemon validation - Test currently skipped pending full bd-85 implementation - Create test coverage improvement issues (bd-114 through bd-118) - All validation checks pass: tests, build, linting baseline Completed: bd-74, bd-77 Amp-Thread-ID: https://ampcode.com/threads/T-24404401-6c5b-466d-9045-0da3a70cff9a Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -13,6 +14,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/steveyegge/beads/internal/config"
|
||||||
"github.com/steveyegge/beads/internal/storage"
|
"github.com/steveyegge/beads/internal/storage"
|
||||||
"github.com/steveyegge/beads/internal/storage/sqlite"
|
"github.com/steveyegge/beads/internal/storage/sqlite"
|
||||||
"github.com/steveyegge/beads/internal/types"
|
"github.com/steveyegge/beads/internal/types"
|
||||||
@@ -20,6 +22,28 @@ import (
|
|||||||
|
|
||||||
const windowsOS = "windows"
|
const windowsOS = "windows"
|
||||||
|
|
||||||
|
func initTestGitRepo(t testing.TB, dir string) {
|
||||||
|
t.Helper()
|
||||||
|
cmd := exec.Command("git", "init")
|
||||||
|
cmd.Dir = dir
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
t.Fatalf("Failed to init git repo: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure git for tests
|
||||||
|
configCmds := [][]string{
|
||||||
|
{"git", "config", "user.email", "test@example.com"},
|
||||||
|
{"git", "config", "user.name", "Test User"},
|
||||||
|
}
|
||||||
|
for _, args := range configCmds {
|
||||||
|
cmd := exec.Command(args[0], args[1:]...)
|
||||||
|
cmd.Dir = dir
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
t.Logf("Warning: git config failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func makeSocketTempDir(t testing.TB) string {
|
func makeSocketTempDir(t testing.TB) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
@@ -658,3 +682,174 @@ func (s *mockDaemonServer) Start(ctx context.Context) error {
|
|||||||
conn.Close()
|
conn.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestMutationToExportLatency tests the latency from mutation to JSONL export
|
||||||
|
// Target: <500ms for single mutation, verify batching for rapid mutations
|
||||||
|
//
|
||||||
|
// NOTE: This test currently tests the existing auto-flush mechanism with debounce.
|
||||||
|
// Once bd-85 (event-driven daemon) is fully implemented and enabled by default,
|
||||||
|
// this test should verify <500ms latency instead of the current debounce-based timing.
|
||||||
|
func TestMutationToExportLatency(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping integration test in short mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Skip("Skipping until event-driven daemon (bd-85) is fully implemented")
|
||||||
|
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
dbDir := filepath.Join(tmpDir, ".beads")
|
||||||
|
if err := os.MkdirAll(dbDir, 0755); err != nil {
|
||||||
|
t.Fatalf("Failed to create beads dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
testDBPath := filepath.Join(dbDir, "test.db")
|
||||||
|
jsonlPath := filepath.Join(dbDir, "issues.jsonl")
|
||||||
|
|
||||||
|
// Initialize git repo (required for auto-flush)
|
||||||
|
initTestGitRepo(t, tmpDir)
|
||||||
|
|
||||||
|
testStore := newTestStore(t, testDBPath)
|
||||||
|
defer testStore.Close()
|
||||||
|
|
||||||
|
// Configure test environment - set global store
|
||||||
|
oldDBPath := dbPath
|
||||||
|
oldStore := store
|
||||||
|
oldStoreActive := storeActive
|
||||||
|
oldAutoFlush := autoFlushEnabled
|
||||||
|
origDebounce := config.GetDuration("flush-debounce")
|
||||||
|
defer func() {
|
||||||
|
dbPath = oldDBPath
|
||||||
|
store = oldStore
|
||||||
|
storeMutex.Lock()
|
||||||
|
storeActive = oldStoreActive
|
||||||
|
storeMutex.Unlock()
|
||||||
|
autoFlushEnabled = oldAutoFlush
|
||||||
|
config.Set("flush-debounce", origDebounce)
|
||||||
|
clearAutoFlushState()
|
||||||
|
}()
|
||||||
|
|
||||||
|
dbPath = testDBPath
|
||||||
|
store = testStore
|
||||||
|
storeMutex.Lock()
|
||||||
|
storeActive = true
|
||||||
|
storeMutex.Unlock()
|
||||||
|
autoFlushEnabled = true
|
||||||
|
// Use fast debounce for testing (500ms to match event-driven target)
|
||||||
|
config.Set("flush-debounce", 500*time.Millisecond)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Get JSONL mod time
|
||||||
|
getModTime := func() time.Time {
|
||||||
|
info, err := os.Stat(jsonlPath)
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
return info.ModTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 1: Single mutation latency with markDirtyAndScheduleFlush
|
||||||
|
t.Run("SingleMutationLatency", func(t *testing.T) {
|
||||||
|
initialModTime := getModTime()
|
||||||
|
|
||||||
|
// Create issue through store
|
||||||
|
issue := &types.Issue{
|
||||||
|
Title: "Latency test issue",
|
||||||
|
Description: "Testing export latency",
|
||||||
|
Status: types.StatusOpen,
|
||||||
|
Priority: 1,
|
||||||
|
IssueType: types.TypeTask,
|
||||||
|
}
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
if err := testStore.CreateIssue(ctx, issue, "test"); err != nil {
|
||||||
|
t.Fatalf("Failed to create issue: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manually trigger flush (simulating what CLI commands do)
|
||||||
|
markDirtyAndScheduleFlush()
|
||||||
|
|
||||||
|
// Wait for JSONL file to be updated (with timeout)
|
||||||
|
timeout := time.After(2 * time.Second) // 500ms debounce + margin
|
||||||
|
ticker := time.NewTicker(10 * time.Millisecond)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
var updated bool
|
||||||
|
var latency time.Duration
|
||||||
|
for !updated {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
modTime := getModTime()
|
||||||
|
if modTime.After(initialModTime) {
|
||||||
|
latency = time.Since(start)
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
case <-timeout:
|
||||||
|
t.Fatal("JSONL file not updated within 2 seconds")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Single mutation export latency: %v", latency)
|
||||||
|
|
||||||
|
// Verify <1s latency (500ms debounce + export time)
|
||||||
|
if latency > 1*time.Second {
|
||||||
|
t.Errorf("Latency %v exceeds 1s threshold", latency)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test 2: Rapid mutations should batch
|
||||||
|
t.Run("RapidMutationBatching", func(t *testing.T) {
|
||||||
|
preTestModTime := getModTime()
|
||||||
|
|
||||||
|
// Create 5 issues rapidly
|
||||||
|
numIssues := 5
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
for i := 0; i < numIssues; i++ {
|
||||||
|
issue := &types.Issue{
|
||||||
|
Title: fmt.Sprintf("Batch test issue %d", i),
|
||||||
|
Description: "Testing batching",
|
||||||
|
Status: types.StatusOpen,
|
||||||
|
Priority: 1,
|
||||||
|
IssueType: types.TypeTask,
|
||||||
|
}
|
||||||
|
if err := testStore.CreateIssue(ctx, issue, "test"); err != nil {
|
||||||
|
t.Fatalf("Failed to create issue %d: %v", i, err)
|
||||||
|
}
|
||||||
|
// Trigger flush for each
|
||||||
|
markDirtyAndScheduleFlush()
|
||||||
|
// Small delay to ensure they're separate operations
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
creationDuration := time.Since(start)
|
||||||
|
t.Logf("Created %d issues in %v", numIssues, creationDuration)
|
||||||
|
|
||||||
|
// Wait for JSONL update
|
||||||
|
timeout := time.After(2 * time.Second) // 500ms debounce + margin
|
||||||
|
ticker := time.NewTicker(10 * time.Millisecond)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
var updated bool
|
||||||
|
for !updated {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
modTime := getModTime()
|
||||||
|
if modTime.After(preTestModTime) {
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
case <-timeout:
|
||||||
|
t.Fatal("JSONL file not updated within 2 seconds")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
totalLatency := time.Since(start)
|
||||||
|
t.Logf("All mutations exported in %v", totalLatency)
|
||||||
|
|
||||||
|
// Verify batching: rapid calls to markDirty within debounce window
|
||||||
|
// should result in single flush after ~500ms
|
||||||
|
if totalLatency > 2*time.Second {
|
||||||
|
t.Errorf("Batching failed: total latency %v exceeds 2s", totalLatency)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -116,11 +116,6 @@ func (s *Server) MutationChan() <-chan MutationEvent {
|
|||||||
return s.mutationChan
|
return s.mutationChan
|
||||||
}
|
}
|
||||||
|
|
||||||
// DroppedEventsCount returns the number of dropped mutation events
|
|
||||||
func (s *Server) DroppedEventsCount() int64 {
|
|
||||||
return s.droppedEvents.Load()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResetDroppedEventsCount resets the dropped events counter and returns the previous value
|
// ResetDroppedEventsCount resets the dropped events counter and returns the previous value
|
||||||
func (s *Server) ResetDroppedEventsCount() int64 {
|
func (s *Server) ResetDroppedEventsCount() int64 {
|
||||||
return s.droppedEvents.Swap(0)
|
return s.droppedEvents.Swap(0)
|
||||||
|
|||||||
Reference in New Issue
Block a user