Files
kandev/apps/backend/internal/agentctl/server/process/manager_temp_test.go
T
2026-07-23 17:24:54 +01:00

229 lines
6.8 KiB
Go

package process
import (
"context"
"errors"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
"github.com/kandev/kandev/internal/agentctl/server/config"
"github.com/kandev/kandev/internal/agentctl/server/shell"
)
func envValue(env []string, key string) string {
prefix := key + "="
for _, item := range env {
if strings.HasPrefix(item, prefix) {
return strings.TrimPrefix(item, prefix)
}
}
return ""
}
func hasEnvValue(env []string, key string) bool {
prefix := key + "="
for _, item := range env {
if strings.HasPrefix(item, prefix) {
return true
}
}
return false
}
func TestManager_BuildFinalCommandPreservesConfiguredTempEnvironment(t *testing.T) {
serviceTemp := setServiceTempTestEnv(t)
mgr := NewManager(&config.InstanceConfig{
WorkDir: t.TempDir(),
AgentArgs: []string{"echo"},
AgentEnv: []string{
"PATH=/usr/bin",
"TMPDIR=/configured/tmpdir",
"TMP=/configured/tmp",
"TEMP=/configured/temp",
},
}, newTestLogger(t))
mgr.adapter = newStubAdapter()
if err := mgr.buildFinalCommand(); err != nil {
t.Fatalf("buildFinalCommand() error = %v", err)
}
want := map[string]string{
"TMPDIR": "/configured/tmpdir",
"TMP": "/configured/tmp",
"TEMP": "/configured/temp",
}
for key, value := range want {
if got := envValue(mgr.cmd.Env, key); got != value {
t.Fatalf("%s = %q, want configured service value %q", key, got, value)
}
}
assertNoAgentTempRoot(t, serviceTemp)
}
func TestManager_BuildFinalCommandLeavesUnsetTempEnvironmentUnset(t *testing.T) {
serviceTemp := setServiceTempTestEnv(t)
mgr := NewManager(&config.InstanceConfig{
WorkDir: t.TempDir(),
AgentArgs: []string{"echo"},
AgentEnv: []string{"PATH=/usr/bin"},
}, newTestLogger(t))
mgr.adapter = newStubAdapter()
if err := mgr.buildFinalCommand(); err != nil {
t.Fatalf("buildFinalCommand() error = %v", err)
}
for _, key := range []string{"TMPDIR", "TMP", "TEMP"} {
if hasEnvValue(mgr.cmd.Env, key) {
t.Fatalf("%s unexpectedly added to child environment: %q", key, mgr.cmd.Env)
}
}
assertNoAgentTempRoot(t, serviceTemp)
}
func TestManager_BeginStopWaitsForInFlightAdmission(t *testing.T) {
mgr := NewManager(&config.InstanceConfig{WorkDir: t.TempDir()}, newTestLogger(t))
release, err := mgr.admitStart()
if err != nil {
t.Fatalf("admitStart() error = %v", err)
}
stopAdmissionDone := make(chan struct{})
go func() {
mgr.BeginStop()
close(stopAdmissionDone)
}()
select {
case <-stopAdmissionDone:
t.Fatal("BeginStop() returned before in-flight admission completed")
default:
}
release()
<-stopAdmissionDone
if err := mgr.Start(context.Background()); !errors.Is(err, ErrManagerStopping) {
t.Fatalf("Start() error = %v, want manager-stopping error", err)
}
if _, err := mgr.StartProcess(context.Background(), StartProcessRequest{}); !errors.Is(err, ErrManagerStopping) {
t.Fatalf("StartProcess() error = %v, want manager-stopping error", err)
}
if err := mgr.StartShell(); !errors.Is(err, ErrManagerStopping) {
t.Fatalf("StartShell() error = %v, want manager-stopping error", err)
}
if err := mgr.StartVscode(context.Background(), "dark"); !errors.Is(err, ErrManagerStopping) {
t.Fatalf("StartVscode() error = %v, want manager-stopping error", err)
}
if _, err := mgr.ShellManager().Start("terminal", shell.DefaultConfig(t.TempDir())); err == nil {
t.Fatal("terminal shell Start() succeeded after BeginStop()")
}
}
func TestManager_TeardownAdmissionDrainHonorsContextAndRetries(t *testing.T) {
mgr := NewManager(&config.InstanceConfig{WorkDir: t.TempDir()}, newTestLogger(t))
release, err := mgr.admitStart()
if err != nil {
t.Fatalf("admitStart() error = %v", err)
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
err = mgr.StopForTeardown(ctx)
if !errors.Is(err, context.Canceled) {
t.Fatalf("StopForTeardown() error = %v, want %v", err, context.Canceled)
}
release()
if err := mgr.StopForTeardown(context.Background()); err != nil {
t.Fatalf("StopForTeardown() retry error = %v", err)
}
}
func TestManager_TeardownWaitsForWorkspaceProcessReap(t *testing.T) {
mgr := NewManager(&config.InstanceConfig{WorkDir: t.TempDir()}, newTestLogger(t))
proc := &commandProcess{
info: ProcessInfo{ID: "workspace-blocked-reap"},
stopSignal: make(chan struct{}),
done: make(chan struct{}),
}
mgr.processRunner.processes[proc.info.ID] = proc
stopDone := make(chan error, 1)
go func() { stopDone <- mgr.StopForTeardown(context.Background()) }()
select {
case <-proc.stopSignal:
case err := <-stopDone:
t.Fatalf("StopForTeardown() returned before workspace process stop/reap: %v", err)
}
select {
case err := <-stopDone:
t.Fatalf("StopForTeardown() returned before workspace process reap: %v", err)
default:
}
close(proc.done)
if err := <-stopDone; err != nil {
t.Fatalf("StopForTeardown() error = %v", err)
}
}
func TestManager_TeardownReportsMainProcessGroupReapFailure(t *testing.T) {
mgr := NewManager(&config.InstanceConfig{WorkDir: t.TempDir()}, newTestLogger(t))
mgr.cmd = &exec.Cmd{Process: &os.Process{Pid: 424242}}
mgr.status.Store(StatusRunning)
mgr.groupAliveFn = func(int) bool { return true }
mgr.terminateGroupFn = func(int) error { return nil }
mgr.killGroupFn = func(int) error { return nil }
mgr.waitGroupExitFn = func(context.Context, int) bool { return false }
t.Cleanup(func() {
mgr.groupAliveFn = func(int) bool { return false }
mgr.waitGroupExitFn = nil
_ = mgr.StopForTeardown(context.Background())
})
err := mgr.StopForTeardown(context.Background())
if err == nil || !strings.Contains(err.Error(), "remains alive") {
t.Fatalf("StopForTeardown() error = %v, want process-group reap failure", err)
}
}
func TestManager_TeardownReportsMainGoroutineReapFailure(t *testing.T) {
mgr := NewManager(&config.InstanceConfig{WorkDir: t.TempDir()}, newTestLogger(t))
mgr.cmd = &exec.Cmd{Process: &os.Process{Pid: 424243}}
mgr.status.Store(StatusRunning)
mgr.wg.Add(1)
t.Cleanup(func() {
mgr.wg.Done()
mgr.managerWaitFn = nil
_ = mgr.StopForTeardown(context.Background())
})
mgr.groupAliveFn = func(int) bool { return false }
mgr.terminateGroupFn = func(int) error { return nil }
mgr.killGroupFn = func(int) error { return nil }
mgr.managerWaitFn = func(context.Context, <-chan struct{}, time.Duration) bool { return false }
err := mgr.StopForTeardown(context.Background())
if err == nil || !strings.Contains(err.Error(), "goroutines were not reaped") {
t.Fatalf("StopForTeardown() error = %v, want manager reap failure", err)
}
}
func setServiceTempTestEnv(t *testing.T) string {
t.Helper()
root := t.TempDir()
for _, key := range []string{"TMPDIR", "TMP", "TEMP"} {
t.Setenv(key, root)
}
return root
}
func assertNoAgentTempRoot(t *testing.T, serviceTemp string) {
t.Helper()
if _, err := os.Stat(filepath.Join(serviceTemp, "kandev-agent")); !os.IsNotExist(err) {
t.Fatalf("unexpected kandev-agent root, err = %v", err)
}
}