scobleclaw/pkg/floxenv/floxenv_test.go
agent-cos 163a3333d3 feat(pkg/floxenv): add Flox environment lifecycle management package
- Activate / Deactivate via flox activate --detach (with graceful SIGTERM→SIGKILL)
- Non-privileged IsAlive process check with Linux /proc fallback
- Dev-mode stub when flox is missing or FLOXENV_DEV_MODE=1
- Unit and integration tests

Closes #16
2026-05-29 23:40:22 -07:00

141 lines
3.4 KiB
Go

package floxenv
import (
"errors"
"os"
"os/exec"
"path/filepath"
"syscall"
"testing"
"time"
)
func TestNewRegistry_DetectsDevMode(t *testing.T) {
r := NewRegistry()
if _, err := exec.LookPath(defaultFloxPath); err == nil && os.Getenv(devModeEnvVar) != "1" {
if r.IsDevMode() {
t.Fatal("expected dev mode false when flox is present and FLOXENV_DEV_MODE unset")
}
} else {
if !r.IsDevMode() {
t.Fatal("expected dev mode true when flox missing or FLOXENV_DEV_MODE=1")
}
}
}
func TestActivate_Stub(t *testing.T) {
// Force dev mode for a deterministic test.
t.Setenv(devModeEnvVar, "1")
tmpDir := t.TempDir()
manifest := filepath.Join(tmpDir, "manifest.toml")
if err := os.WriteFile(manifest, []byte("[install]\n\n"), 0600); err != nil {
t.Fatalf("write manifest: %v", err)
}
r := NewRegistry()
envID, pid, err := r.Activate(manifest)
if err != nil {
t.Fatalf("activate failed: %v", err)
}
if envID == "" {
t.Fatal("expected non-empty envID")
}
if pid <= 0 {
t.Fatalf("expected positive pid, got %d", pid)
}
defer func() {
_ = r.Deactivate(envID, pid)
}()
if !IsAlive(pid) {
t.Fatalf("expected pid %d to be alive after stub activation", pid)
}
}
func TestDeactivate_GracefulShutdown(t *testing.T) {
t.Setenv(devModeEnvVar, "1")
tmpDir := t.TempDir()
manifest := filepath.Join(tmpDir, "manifest.toml")
_ = os.WriteFile(manifest, []byte(""), 0600)
r := NewRegistry()
envID, pid, err := r.Activate(manifest)
if err != nil {
t.Fatalf("activate failed: %v", err)
}
// Deactivate should succeed.
if err := r.Deactivate(envID, pid); err != nil {
t.Fatalf("deactivate failed: %v", err)
}
// Allow scheduler to reap.
time.Sleep(100 * time.Millisecond)
if IsAlive(pid) {
t.Fatalf("expected pid %d to be dead after deactivate", pid)
}
// Registry entry should be removed.
for _, info := range r.List() {
if info.EnvID == envID {
t.Fatalf("registry still holds envID %s after deactivation", envID)
}
}
}
func TestDeactivate_SigkillFallback(t *testing.T) {
t.Setenv(devModeEnvVar, "1")
// Start a child that ignores SIGTERM.
cmd := exec.Command("sh", "-c", `
trap '' TERM
sleep 3600
`)
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
if err := cmd.Start(); err != nil {
t.Fatalf("start ignore-sigterm process: %v", err)
}
pid := cmd.Process.Pid
r := NewRegistry()
envID := "forced-ignore-term"
r.m[envID] = Info{EnvID: envID, PID: pid, ManifestPath: "/tmp/dummy", StartedAt: time.Now()}
err := r.Deactivate(envID, pid)
if err != nil {
// A timeout is acceptable here; the key assertion is that the process
// is no longer alive after Deactivate returns.
if !errors.Is(err, ErrTimeout) {
t.Fatalf("unexpected deactivation error: %v", err)
}
}
// Allow the kernel time to reap.
time.Sleep(200 * time.Millisecond)
if IsAlive(pid) {
t.Fatalf("expected pid %d to be dead after deactivate, but it is still alive", pid)
}
}
func TestIsAlive_InvalidPid(t *testing.T) {
if IsAlive(0) {
t.Fatal("expected pid 0 to be reported dead")
}
if IsAlive(-1) {
t.Fatal("expected pid -1 to be reported dead")
}
// PID 1 should exist on Linux, but in restricted containers it may not.
// We only assert that the function does not panic.
_ = IsAlive(1)
}
func isTimeout(err error) bool {
if err == nil {
return false
}
return err.Error() == ErrTimeout.Error() || err.Error() == (ErrTimeout.Error()+": envID=forced-ignore-term pid=0")
}