Files
agent-exec-transport 7b379d28e7
governance / governance (pull_request) Successful in 17s
ci / vet-and-test (pull_request) Successful in 1m6s
agent-exec transport d2c63b4a83821a5716997a28174233b1/d6e403e1e778
2026-10-18 09:20:03 +00:00

407 lines
9.1 KiB
Go

//go:build unix
package instanceguard
import (
"errors"
"os"
"os/exec"
"path/filepath"
"testing"
"time"
"agent-system-backend/internal/store"
)
func TestAcquireSameResourceAcrossAliases(t *testing.T) {
base := t.TempDir()
real := filepath.Join(base, "real")
if err := os.Mkdir(real, 0o755); err != nil {
t.Fatal(err)
}
link := filepath.Join(base, "link")
if err := os.Symlink(real, link); err != nil {
t.Fatal(err)
}
held, err := Acquire(link)
if err != nil {
t.Fatal(err)
}
defer held.Release()
if _, err := Acquire(real); !errors.Is(err, ErrBusy) {
t.Fatalf("symlink alias: %v", err)
}
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
if err := os.Chdir(base); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = os.Chdir(wd) })
if _, err := Acquire(filepath.Join(".", "real")); !errors.Is(err, ErrBusy) {
t.Fatalf("relative path: %v", err)
}
}
func TestHardlinkRejected(t *testing.T) {
dir := t.TempDir()
db := filepath.Join(dir, "agent-system.db")
st, err := store.OpenSQLite(db)
if err != nil {
t.Fatal(err)
}
if err := st.Close(); err != nil {
t.Fatal(err)
}
if err := os.Link(db, filepath.Join(dir, "alias.db")); err != nil {
t.Fatal(err)
}
if _, err := Acquire(dir); !errors.Is(err, ErrHardlink) {
t.Fatalf("hardlink: %v", err)
}
}
func TestDatabaseReplacementRefusedBeforeOpen(t *testing.T) {
dir := t.TempDir()
st, release, err := openAndBind(t, dir, "")
if err != nil {
t.Fatal(err)
}
db := filepath.Join(dir, "agent-system.db")
release()
_ = st
if err := os.Remove(db); err != nil {
t.Fatal(err)
}
raw, err := os.ReadFile(db + ".lock")
if err != nil {
t.Fatal(err)
}
rec, err := parseRecord(string(raw))
if err != nil || rec == nil {
t.Fatal(err)
}
for i := 0; i < 16; i++ {
if err := os.WriteFile(db, []byte("replaced"), 0o600); err != nil {
t.Fatal(err)
}
_, ino, exists, err := statFile(db)
if err != nil {
t.Fatal(err)
}
if exists && ino != rec.DBIno {
break
}
if err := os.Remove(db); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "inode-burn-"+string(rune('a'+i))), []byte("x"), 0o600); err != nil {
t.Fatal(err)
}
}
lease, err := Acquire(dir)
if err != nil {
t.Fatal(err)
}
defer lease.Release()
if err := lease.AutomaticRefusal(); !errors.Is(err, ErrDBReplaced) {
t.Fatalf("replaced db: %v", err)
}
if _, err := os.Stat(db); err != nil {
t.Fatal(err)
}
}
func TestMissingRecordedDatabaseRefused(t *testing.T) {
dir := t.TempDir()
_, release, err := openAndBind(t, 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)
}
lease, err := Acquire(dir)
if err != nil {
t.Fatal(err)
}
defer lease.Release()
if err := lease.AutomaticRefusal(); !errors.Is(err, ErrDBMissing) {
t.Fatalf("missing db: %v", err)
}
}
func TestRebuiltLockRefusedUntilAdopt(t *testing.T) {
dir := t.TempDir()
st, release, err := openAndBind(t, dir, "inst-a")
if err != nil {
t.Fatal(err)
}
release()
_ = st.Close()
if err := os.Remove(filepath.Join(dir, "agent-system.db.lock")); err != nil {
t.Fatal(err)
}
lease, err := Acquire(dir)
if err != nil {
t.Fatal(err)
}
if err := lease.AutomaticRefusal(); err != nil {
lease.Release()
t.Fatal(err)
}
opened, err := store.OpenSQLite(lease.DBPath())
if err != nil {
lease.Release()
t.Fatal(err)
}
err = Bind(opened, lease, BindOptions{InstanceID: "inst-a"})
_ = opened.Close()
lease.Release()
if !errors.Is(err, ErrLockRebuilt) {
t.Fatalf("rebuilt lock: %v", err)
}
adopted, err := Acquire(dir)
if err != nil {
t.Fatal(err)
}
defer adopted.Release()
opened, err = store.OpenSQLite(adopted.DBPath())
if err != nil {
t.Fatal(err)
}
defer opened.Close()
if err := Bind(opened, adopted, BindOptions{Adopt: true, InstanceID: "inst-a"}); err != nil {
t.Fatal(err)
}
}
func TestClearedLockRecordRefusedWhenInodeStays(t *testing.T) {
dir := t.TempDir()
st, release, err := openAndBind(t, dir, "inst-a")
if err != nil {
t.Fatal(err)
}
release()
_ = st.Close()
lockPath := filepath.Join(dir, "agent-system.db.lock")
if err := os.Truncate(lockPath, 0); err != nil {
t.Fatal(err)
}
lease, err := Acquire(dir)
if err != nil {
t.Fatal(err)
}
opened, err := store.OpenSQLite(lease.DBPath())
if err != nil {
lease.Release()
t.Fatal(err)
}
err = Bind(opened, lease, BindOptions{InstanceID: "inst-a"})
_ = opened.Close()
lease.Release()
if !errors.Is(err, ErrLockRebuilt) {
t.Fatalf("cleared lock: %v", err)
}
}
func TestInstanceMismatchPreservesEmpty(t *testing.T) {
dir := t.TempDir()
st, release, err := openAndBind(t, dir, "inst-a")
if err != nil {
t.Fatal(err)
}
release()
_ = st.Close()
lease, err := Acquire(dir)
if err != nil {
t.Fatal(err)
}
opened, err := store.OpenSQLite(lease.DBPath())
if err != nil {
lease.Release()
t.Fatal(err)
}
err = Bind(opened, lease, BindOptions{InstanceID: "inst-b"})
_ = opened.Close()
lease.Release()
if !errors.Is(err, ErrInstanceMismatch) {
t.Fatalf("mismatch: %v", err)
}
lease, err = Acquire(dir)
if err != nil {
t.Fatal(err)
}
defer lease.Release()
opened, err = store.OpenSQLite(lease.DBPath())
if err != nil {
t.Fatal(err)
}
defer opened.Close()
if err := Bind(opened, lease, BindOptions{}); err != nil {
t.Fatal(err)
}
marker, ok, err := opened.LoadInstanceGuard()
if err != nil || !ok || marker.InstanceID != "inst-a" {
t.Fatalf("empty id should keep marker: ok=%v id=%q err=%v", ok, marker.InstanceID, err)
}
}
func TestLockReleasedAfterKill(t *testing.T) {
if os.Getenv("U8_GUARD_ROLE") == "hold" {
lease, err := Acquire(os.Getenv("U8_GUARD_DIR"))
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(os.Getenv("U8_GUARD_READY"), []byte("ready"), 0o600); err != nil {
t.Fatal(err)
}
time.Sleep(30 * time.Second)
lease.Release()
return
}
dir := t.TempDir()
ready := filepath.Join(dir, "ready")
cmd := exec.Command(os.Args[0], "-test.run=TestLockReleasedAfterKill")
cmd.Env = append(os.Environ(), "U8_GUARD_ROLE=hold", "U8_GUARD_DIR="+dir, "U8_GUARD_READY="+ready)
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()
}
})
deadline := time.Now().Add(10 * time.Second)
for time.Now().Before(deadline) {
if _, err := os.Stat(ready); err == nil {
break
}
time.Sleep(20 * time.Millisecond)
}
if _, err := os.Stat(ready); err != nil {
t.Fatal(err)
}
if _, err := Acquire(dir); !errors.Is(err, ErrBusy) {
t.Fatalf("held: %v", err)
}
if err := cmd.Process.Kill(); err != nil {
t.Fatal(err)
}
_, _ = cmd.Process.Wait()
lease, err := Acquire(dir)
if err != nil {
t.Fatal(err)
}
lease.Release()
}
func TestMissingDataDirIsCreatedBeforeLock(t *testing.T) {
dir := filepath.Join(t.TempDir(), "nested", "data")
lease, err := Acquire(dir)
if err != nil {
t.Fatal(err)
}
lease.Release()
if _, err := os.Stat(dir); err != nil {
t.Fatal(err)
}
}
func TestLockSymlinkCannotAliasDatabase(t *testing.T) {
dir := t.TempDir()
db := filepath.Join(dir, "agent-system.db")
st, err := store.OpenSQLite(db)
if err != nil {
t.Fatal(err)
}
if err := st.Close(); err != nil {
t.Fatal(err)
}
if err := os.Remove(db + ".lock"); err != nil && !os.IsNotExist(err) {
t.Fatal(err)
}
if err := os.Symlink(db, db+".lock"); err != nil {
t.Fatal(err)
}
before, err := os.ReadFile(db)
if err != nil {
t.Fatal(err)
}
if _, err := Acquire(dir); !errors.Is(err, ErrLockAliased) {
t.Fatalf("symlink lock: %v", err)
}
after, err := os.ReadFile(db)
if err != nil {
t.Fatal(err)
}
if string(before) != string(after) {
t.Fatal("aliased lock wrote into the database")
}
}
func TestSymlinkedDatabaseLocksResolvedFile(t *testing.T) {
base := t.TempDir()
realDir := filepath.Join(base, "real")
aliasDir := filepath.Join(base, "alias")
if err := os.Mkdir(realDir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.Mkdir(aliasDir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(realDir, "agent-system.db"), []byte{}, 0o600); err != nil {
t.Fatal(err)
}
if err := os.Symlink(filepath.Join(realDir, "agent-system.db"), filepath.Join(aliasDir, "agent-system.db")); err != nil {
t.Fatal(err)
}
held, err := Acquire(aliasDir)
if err != nil {
t.Fatal(err)
}
defer held.Release()
if _, err := os.Stat(filepath.Join(realDir, "agent-system.db.lock")); err != nil {
t.Fatal(err)
}
if _, err := Acquire(realDir); !errors.Is(err, ErrBusy) {
t.Fatalf("resolved db lock: %v", err)
}
}
func openAndBind(t *testing.T, dir, instanceID string) (*store.Store, func(), error) {
t.Helper()
lease, err := Acquire(dir)
if err != nil {
return nil, nil, err
}
if err := lease.AutomaticRefusal(); err != nil {
lease.Release()
return nil, nil, err
}
st, err := store.OpenSQLite(lease.DBPath())
if err != nil {
lease.Release()
return nil, nil, err
}
if err := Bind(st, lease, BindOptions{InstanceID: instanceID}); err != nil {
_ = st.Close()
lease.Release()
return nil, nil, err
}
return st, func() {
_ = st.Close()
lease.Release()
}, nil
}