Files
kandev/apps/backend/internal/worktree/git_throttle_test.go
T

78 lines
2.8 KiB
Go

package worktree
import (
"context"
"errors"
"os/exec"
"sync"
"testing"
"time"
"github.com/kandev/kandev/internal/common/subproc"
)
// TestRunGitCmd_SaturatedThrottleBlocks verifies that runGitCmd routes
// through the package-wide gitThrottle: once the cap is saturated, the
// next call must block until a holder releases. This is the host-freeze
// mitigation in action — without the throttle, lifecycle bursts
// (worktree add + branch -D + worktree prune + submodule update) for
// several agents starting in parallel could spawn 20+ simultaneous git
// processes, saturating the macOS code-signing fork queue.
//
// Uses gitThrottle.Acquire directly to saturate the pool (no real
// subprocess needed for the saturation half), then `sh -c true` to
// confirm runGitCmd itself goes through the same gate.
func TestRunGitCmd_SaturatedThrottleBlocks(t *testing.T) {
const cap = 2
restore := setGitThrottleCapForTest(cap)
defer restore()
// Saturate the pool out-of-band so the next runGitCmd is forced to
// wait. Holding the slots via gitThrottle.Acquire (the same pool
// runGitCmd uses) is the cleanest way to set this up without timing
// against a real subprocess. Capture each release so the slots are
// returned to the pool when the test ends — keeps the package-wide
// gitThrottle clean for any subsequent test in the same run.
for i := 0; i < cap; i++ {
release, err := subproc.Git().Acquire(context.Background())
if err != nil {
t.Fatalf("pre-saturate acquire %d: %v", i, err)
}
defer release()
}
// A runGitCmd call with a short-deadline ctx must surface the
// throttle's ctx.Err — proving it tried to acquire and got blocked
// rather than exec'ing the subprocess and racing past the throttle.
cctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
err := runGitCmd(cctx, exec.Command("sh", "-c", "true"))
if !errors.Is(err, context.DeadlineExceeded) {
t.Errorf("expected DeadlineExceeded once cap=%d saturated, got %v", cap, err)
}
}
// TestRunGitCmd_RunsToCompletionWhenCapacityAvailable is the positive
// sibling: with capacity free, runGitCmd executes the command and
// returns its result. Guards against a regression where a refactor
// would acquire and immediately error out without running the command.
func TestRunGitCmd_RunsToCompletionWhenCapacityAvailable(t *testing.T) {
restore := setGitThrottleCapForTest(4)
defer restore()
var wg sync.WaitGroup
for i := 0; i < 8; i++ {
wg.Add(1)
go func() {
defer wg.Done()
if err := runGitCmd(context.Background(), exec.Command("sh", "-c", "true")); err != nil {
t.Errorf("runGitCmd unexpected err: %v", err)
}
}()
}
wg.Wait()
}
// Cap parsing tests live in internal/common/subproc/shared_test.go now
// that the env var, default, and resolver are owned by that package.