Files
dingtalk-workspace-cli/internal/helpers/connect_daemon_coverage_more_test.go
T

1007 lines
34 KiB
Go

package helpers
import (
"bytes"
"context"
"errors"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"testing"
"time"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/executor"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/output"
"github.com/spf13/cobra"
)
func preserveDaemonHooks(t *testing.T) {
t.Helper()
oldDetach := daemonDetachEnabled
oldExecutable := daemonExecutable
oldCommand := daemonCommand
oldNow := daemonNow
oldCreateTemp := daemonCreateTemp
oldFileChmod := daemonFileChmod
oldCopy := daemonCopy
oldFileSync := daemonFileSync
oldFileClose := daemonFileClose
oldRename := daemonRename
oldFindProcess := daemonFindProcess
oldProcessAlive := daemonProcessAlive
oldSignalProcess := daemonSignalProcess
oldSignalContext := daemonSignalContext
oldReadDir := connectHealthReadDir
oldDir := connectDaemonDirOverride
oldAfter := helperAfter
oldSleep := helperSleep
t.Cleanup(func() {
daemonDetachEnabled = oldDetach
daemonExecutable = oldExecutable
daemonCommand = oldCommand
daemonNow = oldNow
daemonCreateTemp = oldCreateTemp
daemonFileChmod = oldFileChmod
daemonCopy = oldCopy
daemonFileSync = oldFileSync
daemonFileClose = oldFileClose
daemonRename = oldRename
daemonFindProcess = oldFindProcess
daemonProcessAlive = oldProcessAlive
daemonSignalProcess = oldSignalProcess
daemonSignalContext = oldSignalContext
connectHealthReadDir = oldReadDir
connectDaemonDirOverride = oldDir
helperAfter = oldAfter
helperSleep = oldSleep
})
// Tests exercise the platform-independent lifecycle below the public
// Windows guard. The dedicated unsupported case resets this to false.
daemonDetachEnabled = true
}
func daemonTestCommand() *cobra.Command {
cmd := &cobra.Command{Use: "connect"}
cmd.SetOut(&bytes.Buffer{})
cmd.SetErr(&bytes.Buffer{})
return cmd
}
func instantAfter(time.Duration) <-chan time.Time {
ch := make(chan time.Time, 1)
ch <- time.Now()
return ch
}
func TestCrossPlatformCoverageStartDaemonLifecycleEdges(t *testing.T) {
t.Run("unsupported", func(t *testing.T) {
preserveDaemonHooks(t)
daemonDetachEnabled = false
if err := startDaemon(daemonTestCommand(), "key", "client", "", "custom", "", "", false); err == nil {
t.Fatal("unsupported daemon start succeeded")
}
})
t.Run("directory error", func(t *testing.T) {
preserveDaemonHooks(t)
blocked := filepath.Join(t.TempDir(), "blocked")
if err := os.WriteFile(blocked, []byte("file"), 0o600); err != nil {
t.Fatal(err)
}
connectDaemonDirOverride = blocked
if err := startDaemon(daemonTestCommand(), "key", "client", "", "custom", "", "", false); err == nil {
t.Fatal("daemon start with blocked directory succeeded")
}
})
t.Run("already running", func(t *testing.T) {
preserveDaemonHooks(t)
connectDaemonDirOverride = t.TempDir()
dir, err := connectDaemonDir("key")
if err != nil {
t.Fatal(err)
}
if err := writeDaemonState(dir, daemonState{Pid: os.Getpid(), DirKey: "key"}); err != nil {
t.Fatal(err)
}
if err := startDaemon(daemonTestCommand(), "key", "client", "", "custom", "", "", false); err == nil {
t.Fatal("duplicate daemon start succeeded")
}
})
t.Run("executable error", func(t *testing.T) {
preserveDaemonHooks(t)
connectDaemonDirOverride = t.TempDir()
daemonExecutable = func() (string, error) { return "", errors.New("executable") }
if err := startDaemon(daemonTestCommand(), "key", "client", "", "custom", "", "", false); err == nil {
t.Fatal("daemon start without executable succeeded")
}
})
t.Run("stage error", func(t *testing.T) {
preserveDaemonHooks(t)
connectDaemonDirOverride = t.TempDir()
daemonExecutable = func() (string, error) { return filepath.Join(t.TempDir(), "missing"), nil }
if err := startDaemon(daemonTestCommand(), "key", "client", "", "custom", "", "", false); err == nil {
t.Fatal("daemon start with missing source succeeded")
}
})
t.Run("log error", func(t *testing.T) {
preserveDaemonHooks(t)
connectDaemonDirOverride = t.TempDir()
dir, err := connectDaemonDir("key")
if err != nil {
t.Fatal(err)
}
if err := os.Mkdir(daemonLogPath(dir), 0o700); err != nil {
t.Fatal(err)
}
daemonExecutable = os.Executable
if err := startDaemon(daemonTestCommand(), "key", "client", "", "custom", "", "", false); err == nil {
t.Fatal("daemon start with directory log succeeded")
}
})
t.Run("child start error", func(t *testing.T) {
preserveDaemonHooks(t)
connectDaemonDirOverride = t.TempDir()
daemonExecutable = os.Executable
daemonCommand = func(string, ...string) *exec.Cmd {
return exec.Command(filepath.Join(t.TempDir(), "missing"))
}
if err := startDaemon(daemonTestCommand(), "key", "client", "", "custom", "", "", false); err == nil {
t.Fatal("daemon start with invalid command succeeded")
}
})
t.Run("success", func(t *testing.T) {
preserveDaemonHooks(t)
connectDaemonDirOverride = t.TempDir()
daemonExecutable = os.Executable
fixture := writeShellExecutable(t, t.TempDir(), "daemon-success", "exit 0\n")
daemonCommand = func(string, ...string) *exec.Cmd { return exec.Command(fixture) }
cmd := daemonTestCommand()
var out, errOut bytes.Buffer
cmd.SetOut(&out)
cmd.SetErr(&errOut)
if err := startDaemon(cmd, "key", "client", "app", "custom", "staff", "profile", true); err != nil {
t.Fatalf("startDaemon() error = %v", err)
}
// The streaming root remains legacy until a dedicated stream contract.
if !strings.Contains(out.String(), "daemon started") || !strings.Contains(out.String(), "pid") {
t.Fatalf("legacy daemon start output = %q", out.String())
}
if errOut.Len() != 0 {
t.Fatalf("legacy daemon start stderr = %q", errOut.String())
}
// startDaemon intentionally releases its detached child. On Windows the
// child keeps daemon.log locked until it exits, so wait for that handle
// to close before TempDir cleanup.
dir, err := connectDaemonDir("key")
if err != nil {
t.Fatal(err)
}
deadline := time.Now().Add(2 * time.Second)
for {
err = os.Remove(daemonLogPath(dir))
if err == nil || os.IsNotExist(err) {
break
}
if time.Now().After(deadline) {
t.Fatalf("daemon log remained locked: %v", err)
}
time.Sleep(10 * time.Millisecond)
}
})
}
func TestCrossPlatformCoverageDaemonFileOperationEdges(t *testing.T) {
t.Run("default config directory", func(t *testing.T) {
preserveDaemonHooks(t)
t.Setenv("HOME", t.TempDir())
connectDaemonDirOverride = ""
if _, err := connectDaemonDir("default"); err != nil {
t.Fatal(err)
}
})
for _, tc := range []struct {
name string
configure func()
}{
{"create temp", func() {
daemonCreateTemp = func(string, string) (*os.File, error) { return nil, errors.New("create") }
}},
{"chmod", func() {
daemonFileChmod = func(*os.File, os.FileMode) error { return errors.New("chmod") }
}},
{"copy", func() {
daemonCopy = func(io.Writer, io.Reader) (int64, error) { return 0, errors.New("copy") }
}},
{"sync", func() {
daemonFileSync = func(*os.File) error { return errors.New("sync") }
}},
{"close", func() {
daemonFileClose = func(file *os.File) error {
_ = file.Close()
return errors.New("close")
}
}},
{"rename", func() {
daemonRename = func(string, string) error { return errors.New("rename") }
}},
} {
t.Run(tc.name, func(t *testing.T) {
preserveDaemonHooks(t)
dir := t.TempDir()
src := filepath.Join(dir, "source")
if err := os.WriteFile(src, []byte("binary"), 0o700); err != nil {
t.Fatal(err)
}
tc.configure()
if _, err := stageDaemonExecutable(src, dir); err == nil {
t.Fatalf("stage with %s failure succeeded", tc.name)
}
})
}
t.Run("state write and rename errors", func(t *testing.T) {
preserveDaemonHooks(t)
if err := writeDaemonState(filepath.Join(t.TempDir(), "missing"), daemonState{}); err == nil {
t.Fatal("state write to missing directory succeeded")
}
dir := t.TempDir()
daemonRename = func(string, string) error { return errors.New("rename") }
if err := writeDaemonState(dir, daemonState{}); err == nil {
t.Fatal("state rename failure succeeded")
}
})
t.Run("state read non-not-exist error", func(t *testing.T) {
preserveDaemonHooks(t)
dir := t.TempDir()
if err := os.Mkdir(daemonStatePath(dir), 0o700); err != nil {
t.Fatal(err)
}
if _, err := readDaemonState(dir); err == nil {
t.Fatal("reading directory as state succeeded")
}
})
if backoffDelay(1, 2*time.Second, time.Second) != time.Second {
t.Fatal("backoff base larger than cap was not capped")
}
if statusHintArgs("client", "app-id") != " --robot-client-id client" ||
statusHintArgs("", "app-id") != " --unified-app-id id" || statusHintArgs("", "plain") != "" {
t.Fatal("status hint variants mismatch")
}
workerArgs := buildWorkerArgs([]string{"keep", "--daemon=true", "--daemon-supervise=false", "--daemon-worker=true"})
if strings.Contains(strings.Join(workerArgs, " "), "=true") || workerArgs[0] != "keep" {
t.Fatalf("worker args with assigned daemon flags = %#v", workerArgs)
}
}
func TestCrossPlatformCoverageRunSupervisorLifecycleEdges(t *testing.T) {
t.Run("missing key", func(t *testing.T) {
preserveDaemonHooks(t)
t.Setenv("DWS_CONNECT_DAEMON_DIRKEY", "")
if err := runSupervisor(daemonTestCommand()); err == nil {
t.Fatal("supervisor without key succeeded")
}
})
t.Run("directory error", func(t *testing.T) {
preserveDaemonHooks(t)
blocked := filepath.Join(t.TempDir(), "blocked")
if err := os.WriteFile(blocked, []byte("file"), 0o600); err != nil {
t.Fatal(err)
}
t.Setenv("DWS_CONNECT_DAEMON_DIRKEY", "key")
t.Setenv("DWS_CONNECT_DAEMON_DIR", blocked)
if err := runSupervisor(daemonTestCommand()); err == nil {
t.Fatal("supervisor with blocked directory succeeded")
}
})
t.Run("state write error", func(t *testing.T) {
preserveDaemonHooks(t)
base := t.TempDir()
connectDaemonDirOverride = base
dir, err := connectDaemonDir("key")
if err != nil {
t.Fatal(err)
}
if err := os.Mkdir(daemonStatePath(dir), 0o700); err != nil {
t.Fatal(err)
}
t.Setenv("DWS_CONNECT_DAEMON_DIRKEY", "key")
t.Setenv("DWS_CONNECT_DAEMON_DIR", base)
if err := runSupervisor(daemonTestCommand()); err == nil {
t.Fatal("supervisor with unwritable state succeeded")
}
})
t.Run("executable error", func(t *testing.T) {
preserveDaemonHooks(t)
base := t.TempDir()
t.Setenv("DWS_CONNECT_DAEMON_DIRKEY", "key")
t.Setenv("DWS_CONNECT_DAEMON_DIR", base)
daemonExecutable = func() (string, error) { return "", errors.New("executable") }
if err := runSupervisor(daemonTestCommand()); err == nil {
t.Fatal("supervisor without executable succeeded")
}
})
t.Run("cancel before worker", func(t *testing.T) {
preserveDaemonHooks(t)
base := t.TempDir()
t.Setenv("DWS_CONNECT_DAEMON_DIRKEY", "key")
t.Setenv("DWS_CONNECT_DAEMON_DIR", base)
daemonSignalContext = func() (context.Context, context.CancelFunc) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
return ctx, func() {}
}
if err := runSupervisor(daemonTestCommand()); err != nil {
t.Fatalf("cancelled supervisor = %v", err)
}
})
t.Run("single worker without always-on", func(t *testing.T) {
preserveDaemonHooks(t)
base := t.TempDir()
t.Setenv("DWS_CONNECT_DAEMON_DIRKEY", "key")
t.Setenv("DWS_CONNECT_DAEMON_DIR", base)
t.Setenv("DWS_CONNECT_DAEMON_CLIENTID", "client")
t.Setenv("DWS_CONNECT_DAEMON_UNIFIEDAPPID", "app")
t.Setenv("DWS_CONNECT_DAEMON_CHANNEL", "custom")
t.Setenv("DWS_CONNECT_DAEMON_PROFILE", "profile")
t.Setenv("DWS_CONNECT_DAEMON_ALWAYSON", "")
daemonCommand = func(string, ...string) *exec.Cmd { return exec.Command("sh", "-c", "exit 0") }
if err := runSupervisor(daemonTestCommand()); err != nil {
t.Fatalf("single-worker supervisor = %v", err)
}
})
t.Run("repeated start failures", func(t *testing.T) {
preserveDaemonHooks(t)
base := t.TempDir()
t.Setenv("DWS_CONNECT_DAEMON_DIRKEY", "key")
t.Setenv("DWS_CONNECT_DAEMON_DIR", base)
t.Setenv("DWS_CONNECT_DAEMON_ALWAYSON", "true")
daemonCommand = func(string, ...string) *exec.Cmd { return exec.Command(filepath.Join(base, "missing")) }
helperAfter = instantAfter
if err := runSupervisor(daemonTestCommand()); err == nil {
t.Fatal("supervisor did not give up after start failures")
}
})
t.Run("repeated fast crashes", func(t *testing.T) {
preserveDaemonHooks(t)
base := t.TempDir()
t.Setenv("DWS_CONNECT_DAEMON_DIRKEY", "key")
t.Setenv("DWS_CONNECT_DAEMON_DIR", base)
t.Setenv("DWS_CONNECT_DAEMON_ALWAYSON", "true")
daemonCommand = func(string, ...string) *exec.Cmd { return exec.Command("sh", "-c", "exit 1") }
helperAfter = instantAfter
if err := runSupervisor(daemonTestCommand()); err == nil {
t.Fatal("supervisor did not give up after fast crashes")
}
})
t.Run("healthy crash resets failures then cancellation", func(t *testing.T) {
preserveDaemonHooks(t)
base := t.TempDir()
t.Setenv("DWS_CONNECT_DAEMON_DIRKEY", "key")
t.Setenv("DWS_CONNECT_DAEMON_DIR", base)
t.Setenv("DWS_CONNECT_DAEMON_ALWAYSON", "true")
secondWorker := make(chan struct{})
var workers int
daemonCommand = func(string, ...string) *exec.Cmd {
workers++
if workers == 1 {
return exec.Command("sh", "-c", "exit 0")
}
select {
case <-secondWorker:
default:
close(secondWorker)
}
return exec.Command("sh", "-c", "sleep 5")
}
baseTime := time.Now()
var tick int
daemonNow = func() time.Time {
tick++
if tick >= 3 {
return baseTime.Add(daemonHealthyAfter)
}
return baseTime
}
daemonSignalContext = func() (context.Context, context.CancelFunc) {
ctx, cancel := context.WithCancel(context.Background())
go func() {
<-secondWorker
cancel()
}()
return ctx, cancel
}
if err := runSupervisor(daemonTestCommand()); err != nil {
t.Fatalf("healthy cancellation supervisor = %v", err)
}
})
t.Run("cancel during backoff", func(t *testing.T) {
preserveDaemonHooks(t)
base := t.TempDir()
t.Setenv("DWS_CONNECT_DAEMON_DIRKEY", "key")
t.Setenv("DWS_CONNECT_DAEMON_DIR", base)
t.Setenv("DWS_CONNECT_DAEMON_ALWAYSON", "true")
daemonCommand = func(string, ...string) *exec.Cmd { return exec.Command("sh", "-c", "exit 1") }
never := make(chan time.Time)
enteredBackoff := make(chan struct{})
helperAfter = func(time.Duration) <-chan time.Time {
select {
case <-enteredBackoff:
default:
close(enteredBackoff)
}
return never
}
daemonSignalContext = func() (context.Context, context.CancelFunc) {
ctx, cancel := context.WithCancel(context.Background())
go func() {
<-enteredBackoff
cancel()
}()
return ctx, cancel
}
if err := runSupervisor(daemonTestCommand()); err != nil {
t.Fatalf("backoff cancellation supervisor = %v", err)
}
})
}
func TestCrossPlatformCoverageSuperviseWaitForcedKill(t *testing.T) {
preserveDaemonHooks(t)
worker := exec.Command("sh", "-c", "trap '' TERM; sleep 5")
if err := worker.Start(); err != nil {
t.Fatal(err)
}
time.Sleep(30 * time.Millisecond)
ctx, cancel := context.WithCancel(context.Background())
cancel()
helperAfter = instantAfter
_ = superviseWait(ctx, worker)
}
func TestCrossPlatformCoverageDaemonNotifyStateChangeEdges(t *testing.T) {
preserveDaemonHooks(t)
daemonExecutable = func() (string, error) { return "", errors.New("missing") }
daemonNotifyStateChange("staff", "custom", "client", "started", "")
daemonExecutable = func() (string, error) { return "/bin/sh", nil }
done := make(chan struct{}, 4)
daemonCommand = func(string, ...string) *exec.Cmd {
done <- struct{}{}
return exec.Command("sh", "-c", "exit 0")
}
for _, event := range []string{"started", "stopped", "crashed", "gave_up"} {
daemonNotifyStateChange("staff", "custom", "client", event, "detail")
}
for range 4 {
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("notification subprocess was not created")
}
}
time.Sleep(50 * time.Millisecond)
}
func TestCrossPlatformCoverageDaemonStatusAndStopEdges(t *testing.T) {
t.Run("status directory and corrupt files", func(t *testing.T) {
preserveDaemonHooks(t)
blocked := filepath.Join(t.TempDir(), "blocked")
if err := os.WriteFile(blocked, []byte("file"), 0o600); err != nil {
t.Fatal(err)
}
connectDaemonDirOverride = blocked
if err := daemonStatus(&bytes.Buffer{}, "key", false); err == nil {
t.Fatal("status with blocked directory succeeded")
}
connectDaemonDirOverride = t.TempDir()
dir, _ := connectDaemonDir("corrupt-state")
if err := os.WriteFile(daemonStatePath(dir), []byte("{"), 0o600); err != nil {
t.Fatal(err)
}
if err := daemonStatus(&bytes.Buffer{}, "corrupt-state", false); err == nil {
t.Fatal("status with corrupt state succeeded")
}
dir, _ = connectDaemonDir("corrupt-heartbeat")
if err := os.WriteFile(connectHeartbeatPath(dir), []byte("{"), 0o600); err != nil {
t.Fatal(err)
}
if err := daemonStatus(&bytes.Buffer{}, "corrupt-heartbeat", false); err == nil {
t.Fatal("status with corrupt heartbeat succeeded")
}
})
t.Run("status detailed plain and json", func(t *testing.T) {
preserveDaemonHooks(t)
connectDaemonDirOverride = t.TempDir()
base := time.Now()
daemonNow = func() time.Time { return base }
dir, _ := connectDaemonDir("detailed")
if err := writeDaemonState(dir, daemonState{Pid: os.Getpid(), DirKey: "detailed"}); err != nil {
t.Fatal(err)
}
seedHeartbeat(t, "detailed", connectHeartbeat{
Pid: os.Getpid(), ClientID: "client", Channel: "custom",
StartUnix: base.Add(-time.Minute).Unix(), ConnectedUnix: base.Add(-time.Minute).Unix(),
LastPushUnix: base.Add(-time.Second).Unix(), LastError: "last error",
})
var plain bytes.Buffer
if err := daemonStatus(&plain, "detailed", false); err != nil {
t.Fatal(err)
}
for _, want := range []string{"state", "detail", "pid", "channel", "client", "uptime", "recv", "error", "logs"} {
if !strings.Contains(plain.String(), want) {
t.Errorf("plain status missing %q: %s", want, plain.String())
}
}
var jsonOut bytes.Buffer
if err := daemonStatus(&jsonOut, "detailed", true); err != nil || !strings.Contains(jsonOut.String(), `"state"`) {
t.Fatalf("json status = %q, %v", jsonOut.String(), err)
}
})
t.Run("stop directory and corrupt state", func(t *testing.T) {
preserveDaemonHooks(t)
blocked := filepath.Join(t.TempDir(), "blocked")
if err := os.WriteFile(blocked, []byte("file"), 0o600); err != nil {
t.Fatal(err)
}
connectDaemonDirOverride = blocked
if _, err := daemonStopResult(&bytes.Buffer{}, "key"); err == nil {
t.Fatal("stop with blocked directory succeeded")
}
connectDaemonDirOverride = t.TempDir()
dir, _ := connectDaemonDir("corrupt")
if err := os.WriteFile(daemonStatePath(dir), []byte("{"), 0o600); err != nil {
t.Fatal(err)
}
if _, err := daemonStopResult(&bytes.Buffer{}, "corrupt"); err == nil {
t.Fatal("stop with corrupt state succeeded")
}
})
t.Run("find error", func(t *testing.T) {
preserveDaemonHooks(t)
connectDaemonDirOverride = t.TempDir()
dir, _ := connectDaemonDir("find-error")
if err := writeDaemonState(dir, daemonState{Pid: 123, DirKey: "find-error"}); err != nil {
t.Fatal(err)
}
daemonProcessAlive = func(int) bool { return true }
daemonFindProcess = func(int) (*os.Process, error) { return nil, errors.New("find") }
if _, err := daemonStopResult(&bytes.Buffer{}, "find-error"); err == nil {
t.Fatal("find process error was ignored")
}
})
}
func TestCrossPlatformCoverageDaemonStopHookedLifecycleEdges(t *testing.T) {
const (
supervisorPID = 101
workerPID = 202
)
prepare := func(t *testing.T, key string, withWorker bool) {
t.Helper()
preserveDaemonHooks(t)
connectDaemonDirOverride = t.TempDir()
dir, err := connectDaemonDir(key)
if err != nil {
t.Fatal(err)
}
if err := writeDaemonState(dir, daemonState{Pid: supervisorPID, DirKey: key}); err != nil {
t.Fatal(err)
}
if withWorker {
seedHeartbeat(t, key, connectHeartbeat{Pid: workerPID})
}
daemonFindProcess = func(int) (*os.Process, error) { return new(os.Process), nil }
}
t.Run("orphan worker exits gracefully", func(t *testing.T) {
prepare(t, "orphan-graceful", true)
workerChecks := 0
daemonProcessAlive = func(pid int) bool {
if pid == supervisorPID {
return false
}
workerChecks++
return workerChecks <= 2
}
base := time.Now()
daemonNow = func() time.Time { return base }
var signals []os.Signal
daemonSignalProcess = func(_ *os.Process, signal os.Signal) error {
signals = append(signals, signal)
return nil
}
sleeps := 0
helperSleep = func(time.Duration) { sleeps++ }
if _, err := daemonStopResult(&bytes.Buffer{}, "orphan-graceful"); err != nil {
t.Fatal(err)
}
if len(signals) != 1 || signals[0] != syscall.SIGTERM || sleeps != 1 {
t.Fatalf("signals=%v sleeps=%d", signals, sleeps)
}
})
t.Run("orphan worker is force killed", func(t *testing.T) {
prepare(t, "orphan-force-hooked", true)
daemonProcessAlive = func(pid int) bool { return pid == workerPID }
base := time.Now()
nowCalls := 0
daemonNow = func() time.Time {
nowCalls++
if nowCalls <= 2 {
return base
}
return base.Add(daemonStopTimeout + time.Second)
}
var signals []os.Signal
daemonSignalProcess = func(_ *os.Process, signal os.Signal) error {
signals = append(signals, signal)
return nil
}
helperSleep = func(time.Duration) {}
if _, err := daemonStopResult(&bytes.Buffer{}, "orphan-force-hooked"); err != nil {
t.Fatal(err)
}
if len(signals) != 2 || signals[0] != syscall.SIGTERM || signals[1] != syscall.SIGKILL {
t.Fatalf("signals=%v", signals)
}
})
t.Run("live supervisor exits gracefully", func(t *testing.T) {
prepare(t, "live-graceful-hooked", false)
aliveCalls := 0
daemonProcessAlive = func(int) bool {
aliveCalls++
return aliveCalls == 1
}
base := time.Now()
daemonNow = func() time.Time { return base }
daemonSignalProcess = func(*os.Process, os.Signal) error { return nil }
if _, err := daemonStopResult(&bytes.Buffer{}, "live-graceful-hooked"); err != nil {
t.Fatal(err)
}
})
t.Run("live supervisor signal fails", func(t *testing.T) {
prepare(t, "live-signal-error-hooked", false)
daemonProcessAlive = func(int) bool { return true }
daemonSignalProcess = func(*os.Process, os.Signal) error { return errors.New("signal") }
if _, err := daemonStopResult(&bytes.Buffer{}, "live-signal-error-hooked"); err == nil {
t.Fatal("signal error was ignored")
}
})
t.Run("live supervisor is force killed", func(t *testing.T) {
prepare(t, "live-force-hooked", false)
daemonProcessAlive = func(int) bool { return true }
base := time.Now()
nowCalls := 0
daemonNow = func() time.Time {
nowCalls++
if nowCalls <= 2 {
return base
}
return base.Add(daemonStopTimeout + time.Second)
}
var signals []os.Signal
daemonSignalProcess = func(_ *os.Process, signal os.Signal) error {
signals = append(signals, signal)
return nil
}
helperSleep = func(time.Duration) {}
if _, err := daemonStopResult(&bytes.Buffer{}, "live-force-hooked"); err != nil {
t.Fatal(err)
}
if len(signals) != 2 || signals[0] != syscall.SIGTERM || signals[1] != syscall.SIGKILL {
t.Fatalf("signals=%v", signals)
}
})
}
type daemonSequenceRunner struct {
responses []map[string]any
calls int
}
func (r *daemonSequenceRunner) Run(_ context.Context, invocation executor.Invocation) (executor.Result, error) {
index := r.calls
r.calls++
response := map[string]any{}
if index < len(r.responses) {
response = r.responses[index]
}
return executor.Result{Invocation: invocation, Response: response}, nil
}
func TestCrossPlatformCoverageDaemonListAndNamePaginationEdges(t *testing.T) {
preserveDaemonHooks(t)
connectDaemonDirOverride = t.TempDir()
cmd := &cobra.Command{Use: "list"}
runner := &daemonSequenceRunner{responses: []map[string]any{
{"items": []any{map[string]any{"id": "u-1", "appName": "App One"}}, "hasMore": true, "nextCursor": "next"},
{"items": []any{map[string]any{"unifiedAppId": "u-2", "name": "App Two"}}, "hasMore": false},
}}
names, err := devAppNameMap(cmd, runner)
if err != nil || names["u-1"] != "App One" || names["u-2"] != "App Two" || runner.calls != 2 {
t.Fatalf("paginated names = %#v calls=%d err=%v", names, runner.calls, err)
}
runner = &daemonSequenceRunner{responses: []map[string]any{{"hasMore": true}}}
if _, err := devAppNameMap(cmd, runner); err != nil || runner.calls != 1 {
t.Fatalf("empty cursor pagination calls=%d err=%v", runner.calls, err)
}
reports := []connectHealthReport{{UnifiedAppID: "u-1"}, {UnifiedAppID: "missing"}, {ClientID: "client"}}
runner = &daemonSequenceRunner{responses: []map[string]any{{
"items": []any{map[string]any{"unifiedAppId": "u-1", "name": "Resolved"}}, "hasMore": false,
}}}
resolveAppNames(cmd, runner, reports)
if reports[0].AppName != "Resolved" || reports[1].AppName != "" {
t.Fatalf("resolved reports = %#v", reports)
}
resolveAppNames(cmd, connectResponseRunner{err: errors.New("offline")}, []connectHealthReport{{UnifiedAppID: "u-1"}})
list := prepareUnifiedTestCommand(newDevAppRobotConnectListCommand(runner))
var out bytes.Buffer
list.SetOut(&out)
if err := list.Execute(); err != nil || strings.TrimSpace(out.String()) != "no connectors found" {
t.Fatalf("empty list output = %q, %v", out.String(), err)
}
list = prepareUnifiedTestCommand(newDevAppRobotConnectListCommand(runner))
out.Reset()
list.SetOut(&out)
list.SetArgs([]string{"--json"})
if err := list.Execute(); err != nil || strings.TrimSpace(out.String()) != "[]" {
t.Fatalf("json list array = %q, %v", out.String(), err)
}
seedHeartbeat(t, "listed", connectHeartbeat{
Pid: os.Getpid(), ClientID: strings.Repeat("c", 80), Channel: strings.Repeat("x", 80),
StartUnix: time.Now().Add(-time.Minute).Unix(), ConnectedUnix: time.Now().Add(-time.Minute).Unix(),
})
list = prepareUnifiedTestCommand(newDevAppRobotConnectListCommand(runner))
out.Reset()
list.SetOut(&out)
if err := list.Execute(); err != nil || !strings.Contains(out.String(), "STATE") {
t.Fatalf("table list = %q, %v", out.String(), err)
}
connectHealthReadDir = func(string) ([]os.DirEntry, error) {
return nil, errors.New("read directory")
}
list = prepareUnifiedTestCommand(newDevAppRobotConnectListCommand(runner))
list.SetOut(&bytes.Buffer{})
if err := list.Execute(); err == nil {
t.Fatal("list with blocked directory succeeded")
}
}
func TestCrossPlatformCoverageDaemonControlCommandEdges(t *testing.T) {
preserveDaemonHooks(t)
connectDaemonDirOverride = t.TempDir()
baseDir := connectDaemonDirOverride
defaultProcessAlive := daemonProcessAlive
defaultFindProcess := daemonFindProcess
for _, command := range []*cobra.Command{newDevAppRobotConnectStatusCommand(), newDevAppRobotConnectStopCommand(), newDevAppRobotConnectRestartCommand()} {
command = prepareUnifiedTestCommand(command)
command.SetArgs(nil)
command.SetOut(&bytes.Buffer{})
command.SetErr(&bytes.Buffer{})
if err := command.Execute(); err == nil {
t.Errorf("%s without identity succeeded", command.Name())
}
}
status := prepareUnifiedTestCommand(newDevAppRobotConnectStatusCommand())
status.SetArgs([]string{"--robot-client-id", "missing", "--json"})
status.SetOut(&bytes.Buffer{})
if err := status.Execute(); err != nil {
t.Fatalf("status command = %v", err)
}
stop := prepareUnifiedTestCommand(newDevAppRobotConnectStopCommand())
stop.SetArgs([]string{"--unified-app-id", "missing", "--yes"})
stop.SetOut(&bytes.Buffer{})
if err := stop.Execute(); err != nil {
t.Fatalf("stop command = %v", err)
}
restart := prepareUnifiedTestCommand(newDevAppRobotConnectRestartCommand())
restart.SetArgs([]string{"--robot-client-id", "missing"})
restart.SetOut(&bytes.Buffer{})
restart.SetErr(&bytes.Buffer{})
if err := restart.Execute(); err == nil {
t.Fatal("restart without state succeeded")
}
blocked := filepath.Join(t.TempDir(), "blocked")
if err := os.WriteFile(blocked, []byte("file"), 0o600); err != nil {
t.Fatal(err)
}
connectDaemonDirOverride = blocked
restart = prepareUnifiedTestCommand(newDevAppRobotConnectRestartCommand())
restart.SetArgs([]string{"--robot-client-id", "blocked"})
restart.SetOut(&bytes.Buffer{})
restart.SetErr(&bytes.Buffer{})
if err := restart.Execute(); err == nil {
t.Fatal("restart with blocked directory succeeded")
}
connectDaemonDirOverride = baseDir
corruptDir, _ := connectDaemonDir("corrupt-restart")
if err := os.WriteFile(daemonStatePath(corruptDir), []byte("{"), 0o600); err != nil {
t.Fatal(err)
}
restart = prepareUnifiedTestCommand(newDevAppRobotConnectRestartCommand())
restart.SetArgs([]string{"--robot-client-id", "corrupt-restart"})
restart.SetOut(&bytes.Buffer{})
restart.SetErr(&bytes.Buffer{})
if err := restart.Execute(); err == nil {
t.Fatal("restart with corrupt state succeeded")
}
dir, _ := connectDaemonDir("no-unified")
if err := writeDaemonState(dir, daemonState{Pid: deadPid(t), DirKey: "no-unified"}); err != nil {
t.Fatal(err)
}
restart = newDevAppRobotConnectRestartCommand()
restart.SetArgs([]string{"--robot-client-id", "no-unified"})
restart.SetOut(&bytes.Buffer{})
restart.SetErr(&bytes.Buffer{})
if err := restart.Execute(); err == nil {
t.Fatal("restart without unified app ID succeeded")
}
dir, _ = connectDaemonDir("restart")
state := daemonState{Pid: deadPid(t), DirKey: "restart", UnifiedAppID: "app", Channel: "custom", NotifyStaffID: "staff", Profile: "saved", AlwaysOn: true}
if err := writeDaemonState(dir, state); err != nil {
t.Fatal(err)
}
daemonExecutable = func() (string, error) { return "/bin/sh", nil }
daemonCommand = func(string, ...string) *exec.Cmd { return exec.Command("sh", "-c", "exit 0") }
restart = prepareUnifiedTestCommand(newDevAppRobotConnectRestartCommand())
restart.SetArgs([]string{"--robot-client-id", "restart", "--yes"})
restart.SetOut(&bytes.Buffer{})
restart.SetErr(&bytes.Buffer{})
if err := restart.Execute(); err != nil {
t.Fatalf("restart command = %v", err)
}
if err := writeDaemonState(dir, state); err != nil {
t.Fatal(err)
}
daemonProcessAlive = func(int) bool { return true }
daemonFindProcess = func(int) (*os.Process, error) { return nil, errors.New("find") }
restart = prepareUnifiedTestCommand(newDevAppRobotConnectRestartCommand())
restart.SetArgs([]string{"--robot-client-id", "restart", "--yes"})
restart.SetOut(&bytes.Buffer{})
restart.SetErr(&bytes.Buffer{})
if err := restart.Execute(); err != nil {
t.Fatalf("restart after stop warning = %v", err)
}
daemonProcessAlive = defaultProcessAlive
daemonFindProcess = defaultFindProcess
if err := writeDaemonState(dir, state); err != nil {
t.Fatal(err)
}
root := &cobra.Command{Use: "dws"}
ctx, _ := output.WithResultStore(context.Background())
root.SetContext(ctx)
root.PersistentFlags().String("profile", "", "")
root.PersistentFlags().String("format", "json", "")
root.PersistentFlags().Bool("dry-run", false, "")
root.PersistentFlags().Bool("yes", false, "")
root.PersistentPostRunE = func(cmd *cobra.Command, _ []string) error {
_, _, err := output.EmitStoredResult(cmd)
return err
}
restart = newDevAppRobotConnectRestartCommand()
root.AddCommand(restart)
root.SetArgs([]string{"restart", "--robot-client-id", "restart", "--profile", "override", "--yes"})
root.SetOut(&bytes.Buffer{})
root.SetErr(&bytes.Buffer{})
if err := root.Execute(); err != nil {
t.Fatalf("restart with profile override = %v", err)
}
if err := writeDaemonState(dir, state); err != nil {
t.Fatal(err)
}
daemonExecutable = func() (string, error) { return "", errors.New("missing") }
restart = newDevAppRobotConnectRestartCommand()
restart.SetArgs([]string{"--robot-client-id", "restart"})
restart.SetOut(&bytes.Buffer{})
restart.SetErr(&bytes.Buffer{})
if err := restart.Execute(); err == nil {
t.Fatal("restart without executable succeeded")
}
daemonExecutable = func() (string, error) { return "/bin/sh", nil }
daemonCommand = func(string, ...string) *exec.Cmd { return exec.Command("sh", "-c", "exit 1") }
if err := writeDaemonState(dir, state); err != nil {
t.Fatal(err)
}
restart = newDevAppRobotConnectRestartCommand()
restart.SetArgs([]string{"--robot-client-id", "restart"})
restart.SetOut(&bytes.Buffer{})
restart.SetErr(&bytes.Buffer{})
if err := restart.Execute(); err == nil {
t.Fatal("failing restart subprocess succeeded")
}
}
func TestFrameworkConnectControlDryRunPlansAndLegacyListRollback(t *testing.T) {
preserveDaemonHooks(t)
connectDaemonDirOverride = t.TempDir()
stop := prepareUnifiedTestCommand(newDevAppRobotConnectStopCommand())
stop.SetArgs([]string{"--unified-app-id", "app", "--dry-run"})
var stopOut bytes.Buffer
stop.SetOut(&stopOut)
if err := stop.Execute(); err != nil || !strings.Contains(stopOut.String(), `"preview_kind": "plan"`) {
t.Fatalf("stop preview=%q err=%v", stopOut.String(), err)
}
restart := prepareUnifiedTestCommand(newDevAppRobotConnectRestartCommand())
restart.SetArgs([]string{"--unified-app-id", "app", "--dry-run"})
var restartOut bytes.Buffer
restart.SetOut(&restartOut)
if err := restart.Execute(); err != nil || !strings.Contains(restartOut.String(), `"preview_kind": "plan"`) {
t.Fatalf("restart preview=%q err=%v", restartOut.String(), err)
}
dir, err := connectDaemonDir("saved")
if err != nil {
t.Fatal(err)
}
if err := writeDaemonState(dir, daemonState{DirKey: "saved", UnifiedAppID: "saved-app", Channel: "custom"}); err != nil {
t.Fatal(err)
}
restart = prepareUnifiedTestCommand(newDevAppRobotConnectRestartCommand())
restart.SetArgs([]string{"--robot-client-id", "saved", "--dry-run"})
restartOut.Reset()
restart.SetOut(&restartOut)
if err := restart.Execute(); err != nil || !strings.Contains(restartOut.String(), "saved-app") {
t.Fatalf("saved restart preview=%q err=%v", restartOut.String(), err)
}
list := prepareUnifiedTestCommand(newDevAppRobotConnectListCommand(connectResponseRunner{response: map[string]any{"items": []any{}, "hasMore": false}}))
output.SetCommandRollout(list, output.RolloutLegacyOnly)
for _, format := range []string{"json", "table", "pretty"} {
var out bytes.Buffer
list.SetOut(&out)
list.SetArgs([]string{"--format", format})
if err := list.Execute(); err != nil {
t.Fatalf("legacy list %s: %v", format, err)
}
}
}