mirror of
https://github.com/Buriburizaem0n/nezha_domains.git
synced 2026-09-21 10:40:13 +00:00
test(agentcompat): add process supervision harness
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
//go:build linux
|
||||
|
||||
package process
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestSupervisor_CleanExit(t *testing.T) {
|
||||
// Given
|
||||
supervisor := newHelperSupervisor(t.Context(), "clean", nil)
|
||||
|
||||
// When
|
||||
requireNoError(t, supervisor.Start())
|
||||
requireNoError(t, supervisor.WaitReady(t.Context()))
|
||||
|
||||
// Then
|
||||
requireNoError(t, supervisor.Wait(t.Context()))
|
||||
}
|
||||
|
||||
func TestSupervisor_RunsChildWithConfiguredCredential(t *testing.T) {
|
||||
// Given
|
||||
credentialDirectory, err := os.MkdirTemp("/tmp", "agentcompat-credential-")
|
||||
requireNoError(t, err)
|
||||
t.Cleanup(func() { _ = os.RemoveAll(credentialDirectory) })
|
||||
requireNoError(t, os.Chmod(credentialDirectory, 0o777))
|
||||
marker := filepath.Join(credentialDirectory, "credential.txt")
|
||||
supervisor := newHelperSupervisor(t.Context(), "credential", []string{helperMarkerEnv + "=" + marker})
|
||||
testBinary, err := os.ReadFile(os.Args[0])
|
||||
requireNoError(t, err)
|
||||
executablePath := filepath.Join(credentialDirectory, "process-helper")
|
||||
requireNoError(t, os.WriteFile(executablePath, testBinary, 0o755))
|
||||
supervisor.spec.Path = executablePath
|
||||
supervisor.spec.Credential = &syscall.Credential{Uid: 65534, Gid: 65534}
|
||||
|
||||
// When
|
||||
requireNoError(t, supervisor.Start())
|
||||
requireNoError(t, supervisor.Wait(t.Context()))
|
||||
|
||||
// Then
|
||||
content, err := os.ReadFile(marker)
|
||||
requireNoError(t, err)
|
||||
if strings.TrimSpace(string(content)) != "65534:65534" {
|
||||
t.Fatalf("child credential = %q, want 65534:65534", content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSupervisor_KillsProcessTree(t *testing.T) {
|
||||
// Given
|
||||
marker := filepath.Join(t.TempDir(), "grandchild.pid")
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
supervisor := newHelperSupervisor(ctx, "tree", []string{helperMarkerEnv + "=" + marker})
|
||||
requireNoError(t, supervisor.Start())
|
||||
requireNoError(t, supervisor.WaitReady(t.Context()))
|
||||
grandchildPID := readPID(t, marker)
|
||||
processGroupID := supervisor.ProcessGroupID()
|
||||
|
||||
// When
|
||||
cancel()
|
||||
select {
|
||||
case <-supervisor.cleanupDone:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("context cancellation did not complete process-tree cleanup")
|
||||
}
|
||||
|
||||
// Then
|
||||
requirePIDGone(t, supervisor.PID())
|
||||
requirePIDGone(t, grandchildPID)
|
||||
if err := syscall.Kill(-processGroupID, 0); !errors.Is(err, syscall.ESRCH) {
|
||||
t.Fatalf("process group %d remains: %v", processGroupID, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSupervisor_AdoptsListener(t *testing.T) {
|
||||
// Given
|
||||
listener, err := net.Listen("tcp4", "127.0.0.1:0")
|
||||
requireNoError(t, err)
|
||||
tcpListener := listener.(*net.TCPListener)
|
||||
inheritedFile, err := tcpListener.File()
|
||||
requireNoError(t, err)
|
||||
requireNoError(t, tcpListener.Close())
|
||||
supervisor := newHelperSupervisor(t.Context(), "listener", []string{helperFDEnv + "=3"})
|
||||
supervisor.spec.ExtraFiles = []*os.File{inheritedFile}
|
||||
|
||||
// When
|
||||
requireNoError(t, supervisor.Start())
|
||||
requireNoError(t, supervisor.WaitReady(t.Context()))
|
||||
sample, err := SampleProcess(supervisor.PID())
|
||||
requireNoError(t, err)
|
||||
|
||||
// Then
|
||||
if sample.TCPListenerCount != 1 {
|
||||
t.Fatalf("TCP listeners = %d, want 1", sample.TCPListenerCount)
|
||||
}
|
||||
requireNoError(t, supervisor.Stop(t.Context()))
|
||||
requirePIDGone(t, supervisor.PID())
|
||||
}
|
||||
|
||||
func TestSupervisor_RedactsLogs(t *testing.T) {
|
||||
// Given
|
||||
var output bytes.Buffer
|
||||
supervisor := newHelperSupervisor(t.Context(), "logs", nil)
|
||||
supervisor.spec.MaxLogBytes = 128
|
||||
supervisor.spec.Stdout = &output
|
||||
supervisor.spec.Stderr = &output
|
||||
|
||||
// When
|
||||
requireNoError(t, supervisor.Start())
|
||||
requireNoError(t, supervisor.WaitReady(t.Context()))
|
||||
requireNoError(t, supervisor.Wait(t.Context()))
|
||||
|
||||
// Then
|
||||
logged := output.String()
|
||||
if strings.Contains(logged, "top-secret") || strings.Contains(logged, "eyJsecret") {
|
||||
t.Fatalf("secret survived supervisor log redaction: %s", logged)
|
||||
}
|
||||
if output.Len() > supervisor.spec.MaxLogBytes*2 {
|
||||
t.Fatalf("combined log bytes = %d, per-stream limit = %d", output.Len(), supervisor.spec.MaxLogBytes)
|
||||
}
|
||||
if !strings.Contains(logged, truncationMarker) {
|
||||
t.Fatalf("truncation marker missing: %q", logged)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSupervisor_RecordsForcedCleanupForSIGTERMIgnoringChild(t *testing.T) {
|
||||
// Given
|
||||
resultsDir := t.TempDir()
|
||||
marker := filepath.Join(t.TempDir(), "forced-grandchild.pid")
|
||||
supervisor := newHelperSupervisor(t.Context(), "force-tree", []string{helperMarkerEnv + "=" + marker})
|
||||
requireNoError(t, supervisor.Start())
|
||||
requireNoError(t, supervisor.WaitReady(t.Context()))
|
||||
grandchildPID := readPID(t, marker)
|
||||
|
||||
// When
|
||||
requireNoError(t, supervisor.Stop(t.Context()))
|
||||
receipt := NewCleanupReceipt([]CleanupRecord{supervisor.CleanupRecord()})
|
||||
receiptPath := filepath.Join(resultsDir, "cleanup.json")
|
||||
requireNoError(t, WriteCleanupReceipt(receiptPath, receipt))
|
||||
|
||||
// Then
|
||||
if !supervisor.ForcedCleanup() {
|
||||
t.Fatal("forced cleanup was not recorded")
|
||||
}
|
||||
data, err := os.ReadFile(receiptPath)
|
||||
requireNoError(t, err)
|
||||
if !strings.Contains(string(data), `"forced": true`) {
|
||||
t.Fatalf("cleanup receipt = %s", data)
|
||||
}
|
||||
requirePIDGone(t, supervisor.PID())
|
||||
requirePIDGone(t, grandchildPID)
|
||||
}
|
||||
|
||||
func TestSupervisor_InterruptSignalCleansProcessTree(t *testing.T) {
|
||||
// Given
|
||||
marker := filepath.Join(t.TempDir(), "interrupt-grandchild.pid")
|
||||
command := exec.Command(os.Args[0], "-test.run=^TestProcessHelper$")
|
||||
command.Env = append(os.Environ(), helperModeEnv+"=interrupt-probe", helperMarkerEnv+"="+marker)
|
||||
output, err := command.StdoutPipe()
|
||||
requireNoError(t, err)
|
||||
command.Stderr = os.Stderr
|
||||
requireNoError(t, command.Start())
|
||||
scanner := bufio.NewScanner(output)
|
||||
ready := false
|
||||
for scanner.Scan() {
|
||||
if scanner.Text() == "PROBE_READY" {
|
||||
ready = true
|
||||
break
|
||||
}
|
||||
}
|
||||
requireNoError(t, scanner.Err())
|
||||
if !ready {
|
||||
t.Fatal("interrupt probe exited before readiness")
|
||||
}
|
||||
leaderPID := readPID(t, marker+".leader")
|
||||
grandchildPID := readPID(t, marker)
|
||||
|
||||
// When
|
||||
requireNoError(t, command.Process.Signal(syscall.SIGTERM))
|
||||
requireNoError(t, command.Wait())
|
||||
|
||||
// Then
|
||||
requirePIDGone(t, leaderPID)
|
||||
requirePIDGone(t, grandchildPID)
|
||||
}
|
||||
Reference in New Issue
Block a user