202 lines
5.2 KiB
Go
202 lines
5.2 KiB
Go
//go:build unix
|
|
|
|
package app
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"agent-system-backend/internal/instanceguard"
|
|
"agent-system-backend/internal/store"
|
|
core "agent-system-core"
|
|
)
|
|
|
|
func TestSecondProcessRejectedBeforeOpen(t *testing.T) {
|
|
if os.Getenv("U8_APP_ROLE") == "hold" {
|
|
holdService(t)
|
|
return
|
|
}
|
|
dir := t.TempDir()
|
|
ready := filepath.Join(dir, "ready")
|
|
cmd := exec.Command(os.Args[0], "-test.run=TestSecondProcessRejectedBeforeOpen")
|
|
cmd.Env = append(os.Environ(), "U8_APP_ROLE=hold", "U8_APP_DIR="+dir, "U8_APP_READY="+ready)
|
|
cmd.Dir = t.TempDir()
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
if err := cmd.Start(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() {
|
|
if cmd.Process != nil {
|
|
_ = cmd.Process.Kill()
|
|
_, _ = cmd.Process.Wait()
|
|
}
|
|
})
|
|
waitReady(t, ready)
|
|
_, _, err := openServiceStore(Options{DataDir: dir, Addr: ":1"})
|
|
if !errors.Is(err, instanceguard.ErrBusy) {
|
|
t.Fatalf("second open: %v", err)
|
|
}
|
|
if err := Migrate(Options{DataDir: dir}); !errors.Is(err, instanceguard.ErrBusy) {
|
|
t.Fatalf("migrate while held: %v", err)
|
|
}
|
|
cli, err := store.OpenExistingSQLite(filepath.Join(dir, "agent-system.db"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
run, err := cli.GetRunByID("run_u8_hold")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if run.Status != core.RunWaitingHITL {
|
|
t.Fatalf("run changed while second process was refused: %s", run.Status)
|
|
}
|
|
if _, err := cli.CreateProject("owner", "short"); err != nil {
|
|
t.Fatalf("account-style write while service holds the guard: %v", err)
|
|
}
|
|
if err := cli.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := cmd.Process.Kill(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, _ = cmd.Process.Wait()
|
|
st, release, err := openServiceStore(Options{DataDir: dir, Addr: ":2"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer release()
|
|
run, err = st.GetRunByID("run_u8_hold")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if run.Status != core.RunWaitingHITL {
|
|
t.Fatalf("restart reconciled a run the second process never opened: %s", run.Status)
|
|
}
|
|
}
|
|
|
|
func TestCopiedBinaryStillSharesGuard(t *testing.T) {
|
|
if os.Getenv("U8_APP_ROLE") == "copy-hold" {
|
|
holdService(t)
|
|
return
|
|
}
|
|
bin, err := os.Executable()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
copyPath := filepath.Join(t.TempDir(), "agent-system-test-copy")
|
|
raw, err := os.ReadFile(bin)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(copyPath, raw, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
dir := t.TempDir()
|
|
ready := filepath.Join(dir, "ready")
|
|
cmd := exec.Command(copyPath, "-test.run=TestCopiedBinaryStillSharesGuard")
|
|
cmd.Env = append(os.Environ(), "U8_APP_ROLE=copy-hold", "U8_APP_DIR="+dir, "U8_APP_READY="+ready)
|
|
cmd.Dir = t.TempDir()
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
if err := cmd.Start(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() {
|
|
if cmd.Process != nil {
|
|
_ = cmd.Process.Kill()
|
|
_, _ = cmd.Process.Wait()
|
|
}
|
|
})
|
|
waitReady(t, ready)
|
|
if _, _, err := openServiceStore(Options{DataDir: dir}); !errors.Is(err, instanceguard.ErrBusy) {
|
|
t.Fatalf("copied binary: %v", err)
|
|
}
|
|
}
|
|
|
|
func holdService(t *testing.T) {
|
|
t.Helper()
|
|
dir := os.Getenv("U8_APP_DIR")
|
|
st, _, err := openServiceStore(Options{DataDir: dir})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
run := &core.Run{
|
|
ID: "run_u8_hold", ConversationID: "c", ProjectID: "p", Owner: "o",
|
|
Kind: core.RunConversation, Status: core.RunWaitingHITL,
|
|
}
|
|
if err := st.CreateRun(run); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(os.Getenv("U8_APP_READY"), []byte("ready"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
time.Sleep(30 * time.Second)
|
|
}
|
|
|
|
func waitReady(t *testing.T, ready string) {
|
|
t.Helper()
|
|
deadline := time.Now().Add(20 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
if _, err := os.Stat(ready); err == nil {
|
|
return
|
|
}
|
|
time.Sleep(20 * time.Millisecond)
|
|
}
|
|
t.Fatal("child did not become ready")
|
|
}
|
|
|
|
func TestAdoptDoesNotCreateMissingDatabase(t *testing.T) {
|
|
dir := t.TempDir()
|
|
_, release, err := openServiceStore(Options{DataDir: dir})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
release()
|
|
db := filepath.Join(dir, "agent-system.db")
|
|
if err := os.Remove(db); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, _, err = openServiceStore(Options{DataDir: dir, AdoptInstanceGuard: true})
|
|
if !errors.Is(err, instanceguard.ErrDBMissing) {
|
|
t.Fatalf("adopt missing: %v", err)
|
|
}
|
|
if _, err := os.Stat(db); !os.IsNotExist(err) {
|
|
t.Fatalf("adopt created a database: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestIdentityFailureDoesNotStick(t *testing.T) {
|
|
dir := t.TempDir()
|
|
t.Setenv("AGENT_SYSTEM_INSTANCE_ID", "wrong")
|
|
if _, _, err := openServiceStore(Options{DataDir: dir}); err == nil {
|
|
t.Fatal("missing identity row was accepted")
|
|
}
|
|
t.Setenv("AGENT_SYSTEM_INSTANCE_ID", "")
|
|
st, err := store.OpenSQLite(filepath.Join(dir, "agent-system.db"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
marker, ok, err := st.LoadInstanceGuard()
|
|
if err != nil || ok || marker.InstanceID != "" {
|
|
t.Fatalf("failed start wrote a guard id: ok=%v id=%q err=%v", ok, marker.InstanceID, err)
|
|
}
|
|
if err := st.EnsureSystemIdentity("right", "sqlite"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := st.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv("AGENT_SYSTEM_INSTANCE_ID", "right")
|
|
opened, release, err := openServiceStore(Options{DataDir: dir})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
release()
|
|
_ = opened
|
|
}
|