Files
goclaw/internal/skills/dep_installer_test.go

147 lines
3.7 KiB
Go

package skills
import (
"context"
"os"
"path/filepath"
"sync"
"sync/atomic"
"testing"
"time"
)
// TestSharedPackageLocker_NilPath verifies that when no shared locker is
// installed, sharedPackageLocker() returns nil (backward-compatible path).
func TestSharedPackageLocker_NilPath(t *testing.T) {
// Clear any previously injected locker from other tests.
sharedLocker.Store(nil)
if got := sharedPackageLocker(); got != nil {
t.Errorf("sharedPackageLocker() = %v, want nil when not set", got)
}
}
func TestInstallSingleDepNpmUsesWritableRuntimePrefix(t *testing.T) {
runtimeDir := t.TempDir()
t.Setenv("RUNTIME_DIR", runtimeDir)
t.Setenv("NPM_CONFIG_PREFIX", "")
t.Setenv("FIXTURE_NPM_EXIT", "0")
useFixtureNpm(t)
ok, msg := InstallSingleDep(context.Background(), "npm:@aiagentwiki/cli")
if !ok {
t.Fatalf("InstallSingleDep failed: %s", msg)
}
wantPrefix := filepath.Join(runtimeDir, "npm-global")
if _, err := os.Stat(wantPrefix); err != nil {
t.Fatalf("npm prefix %q was not created: %v", wantPrefix, err)
}
}
// TestSetSharedPackageLocker_InjectsAndReturns verifies that
// SetSharedPackageLocker stores the locker and sharedPackageLocker retrieves it.
func TestSetSharedPackageLocker_InjectsAndReturns(t *testing.T) {
t.Cleanup(func() { sharedLocker.Store(nil) }) // restore after test
l := NewPackageLocker()
SetSharedPackageLocker(l)
got := sharedPackageLocker()
if got == nil {
t.Fatal("sharedPackageLocker() returned nil after SetSharedPackageLocker")
}
if got != l {
t.Error("sharedPackageLocker() returned a different locker than injected")
}
}
// TestSharedPackageLocker_Serializes verifies that when a shared locker is
// installed, concurrent calls for the same source+pkg key are serialized
// (at most one acquires at a time).
func TestSharedPackageLocker_Serializes(t *testing.T) {
t.Cleanup(func() { sharedLocker.Store(nil) })
l := NewPackageLocker()
SetSharedPackageLocker(l)
const goroutines = 8
var inFlight int32
var maxConcurrent int32
var wg sync.WaitGroup
for i := 0; i < goroutines; i++ {
wg.Add(1)
go func() {
defer wg.Done()
release, err := l.Acquire(context.Background(), "pip", "foo")
if err != nil {
t.Errorf("Acquire failed: %v", err)
return
}
cur := atomic.AddInt32(&inFlight, 1)
// Update peak concurrency.
for {
m := atomic.LoadInt32(&maxConcurrent)
if cur <= m || atomic.CompareAndSwapInt32(&maxConcurrent, m, cur) {
break
}
}
time.Sleep(5 * time.Millisecond)
atomic.AddInt32(&inFlight, -1)
release()
}()
}
wg.Wait()
if maxConcurrent != 1 {
t.Fatalf("expected max concurrency 1, got %d — locker is not serializing", maxConcurrent)
}
}
// TestSharedPackageLocker_DifferentSources verifies that pip and npm keys are
// independent (different sources can hold locks concurrently).
func TestSharedPackageLocker_DifferentSources(t *testing.T) {
t.Cleanup(func() { sharedLocker.Store(nil) })
l := NewPackageLocker()
SetSharedPackageLocker(l)
started := make(chan struct{}, 2)
done := make(chan struct{})
go func() {
release, err := l.Acquire(context.Background(), "pip", "requests")
if err != nil {
t.Errorf("pip Acquire: %v", err)
return
}
started <- struct{}{}
<-done
release()
}()
go func() {
release, err := l.Acquire(context.Background(), "npm", "requests")
if err != nil {
t.Errorf("npm Acquire: %v", err)
return
}
started <- struct{}{}
<-done
release()
}()
// Both goroutines (different source keys) should acquire without blocking.
timer := time.NewTimer(100 * time.Millisecond)
defer timer.Stop()
for i := 0; i < 2; i++ {
select {
case <-started:
case <-timer.C:
t.Fatal("pip and npm locks should be independent — timed out waiting")
}
}
close(done)
}