146 lines
4.4 KiB
Go
146 lines
4.4 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/aoagents/agent-orchestrator/backend/internal/runfile"
|
|
)
|
|
|
|
// TestWaitForStoppedKeepsRunFileFromConcurrentStart guards against deleting a
|
|
// fresh daemon's handshake: if a concurrent `ao start` replaces running.json
|
|
// with a new live PID while we are polling the PID we stopped, waitForStopped
|
|
// must report stopped but leave the new run-file intact.
|
|
func TestWaitForStoppedKeepsRunFileFromConcurrentStart(t *testing.T) {
|
|
dir := t.TempDir()
|
|
runFile := filepath.Join(dir, "running.json")
|
|
|
|
const stoppedPID, newPID = 1111, 2222
|
|
// running.json now belongs to a different, live daemon.
|
|
if err := runfile.Write(runFile, runfile.Info{PID: newPID, Port: 3001, StartedAt: time.Unix(100, 0).UTC()}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
c := &commandContext{deps: Deps{
|
|
ProcessAlive: func(pid int) bool { return pid == newPID }, // stoppedPID is dead
|
|
Now: func() time.Time { return time.Unix(200, 0).UTC() },
|
|
Sleep: func(time.Duration) {},
|
|
}.withDefaults()}
|
|
|
|
st, err := c.waitForStopped(context.Background(), stoppedPID, runFile, dir, time.Second)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if st.State != stateStopped {
|
|
t.Fatalf("state = %q, want stopped", st.State)
|
|
}
|
|
|
|
info, err := runfile.Read(runFile)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if info == nil {
|
|
t.Fatal("new daemon's run-file was deleted by stop of a different PID")
|
|
}
|
|
if info.PID != newPID {
|
|
t.Fatalf("run-file PID = %d, want %d (new daemon)", info.PID, newPID)
|
|
}
|
|
}
|
|
|
|
// TestWaitForStoppedRemovesOwnRunFile confirms the normal path still cleans up:
|
|
// when the dead PID owns the run-file, it is removed.
|
|
func TestWaitForStoppedRemovesOwnRunFile(t *testing.T) {
|
|
dir := t.TempDir()
|
|
runFile := filepath.Join(dir, "running.json")
|
|
|
|
const stoppedPID = 1111
|
|
if err := runfile.Write(runFile, runfile.Info{PID: stoppedPID, Port: 3001, StartedAt: time.Unix(100, 0).UTC()}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
c := &commandContext{deps: Deps{
|
|
ProcessAlive: func(int) bool { return false },
|
|
Now: func() time.Time { return time.Unix(200, 0).UTC() },
|
|
Sleep: func(time.Duration) {},
|
|
}.withDefaults()}
|
|
|
|
st, err := c.waitForStopped(context.Background(), stoppedPID, runFile, dir, time.Second)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if st.State != stateStopped {
|
|
t.Fatalf("state = %q, want stopped", st.State)
|
|
}
|
|
info, err := runfile.Read(runFile)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if info != nil {
|
|
t.Fatalf("own run-file should have been removed, got %#v", info)
|
|
}
|
|
}
|
|
|
|
func TestWaitForStoppedWaitsAfterRunFileRemovedUntilProcessExits(t *testing.T) {
|
|
dir := t.TempDir()
|
|
runFile := filepath.Join(dir, "running.json")
|
|
|
|
const stoppedPID = 1111
|
|
now := time.Unix(200, 0).UTC()
|
|
aliveChecks := 0
|
|
sleeps := 0
|
|
c := &commandContext{deps: Deps{
|
|
ProcessAlive: func(int) bool {
|
|
aliveChecks++
|
|
return aliveChecks < 3
|
|
},
|
|
Now: func() time.Time {
|
|
return now
|
|
},
|
|
Sleep: func(d time.Duration) {
|
|
sleeps++
|
|
now = now.Add(d)
|
|
},
|
|
}.withDefaults()}
|
|
|
|
st, err := c.waitForStopped(context.Background(), stoppedPID, runFile, dir, time.Second)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if st.State != stateStopped {
|
|
t.Fatalf("state = %q, want stopped", st.State)
|
|
}
|
|
if sleeps == 0 {
|
|
t.Fatal("waitForStopped returned before waiting for process exit")
|
|
}
|
|
if aliveChecks < 3 {
|
|
t.Fatalf("process checks = %d, want at least 3", aliveChecks)
|
|
}
|
|
}
|
|
|
|
// TestWaitForStoppedReportsStoppedWhenRunFileGoneButProcessLingers covers
|
|
// issue #2214: once the daemon has removed its run-file (its liveness marker)
|
|
// the stop is committed, so if the process is still draining background workers
|
|
// past the timeout, waitForStopped must report stopped rather than erroring.
|
|
func TestWaitForStoppedReportsStoppedWhenRunFileGoneButProcessLingers(t *testing.T) {
|
|
dir := t.TempDir()
|
|
runFile := filepath.Join(dir, "running.json") // never written: run-file already gone
|
|
|
|
const stoppedPID = 1111
|
|
now := time.Unix(200, 0).UTC()
|
|
c := &commandContext{deps: Deps{
|
|
ProcessAlive: func(int) bool { return true }, // process never exits
|
|
Now: func() time.Time { return now },
|
|
Sleep: func(d time.Duration) { now = now.Add(d) },
|
|
}.withDefaults()}
|
|
|
|
st, err := c.waitForStopped(context.Background(), stoppedPID, runFile, dir, time.Second)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if st.State != stateStopped {
|
|
t.Fatalf("state = %q, want stopped", st.State)
|
|
}
|
|
}
|