mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-13 20:14:50 +00:00
Add comprehensive integration tests for core v3 store APIs: - v3_session_store_ext_test.go: pagination, isolation, rich-list - v3_agent_store_ext_test.go: context files, profiles, overrides, types - v3_agent_links_store_test.go: delegation links CRUD - v3_cron_store_ext_test.go: cron CRUD, enable toggle, run log - v3_vault_store_ext_test.go: vault docs CRUD/search/links - v3_memory_store_ext_test.go: memory BM25 search/isolation
283 lines
7.7 KiB
Go
283 lines
7.7 KiB
Go
//go:build integration
|
|
|
|
package integration
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/nextlevelbuilder/goclaw/internal/store"
|
|
)
|
|
|
|
// TestStoreCron_RemoveJob verifies that RemoveJob deletes the row and is tenant-scoped.
|
|
func TestStoreCron_RemoveJob(t *testing.T) {
|
|
db := testDB(t)
|
|
tenantID, agentID := seedTenantAgent(t, db)
|
|
ctx := tenantCtx(tenantID)
|
|
s := newCronStore(t)
|
|
|
|
everyMS := int64(30000)
|
|
job, err := s.AddJob(ctx, "remove-test-job", store.CronSchedule{
|
|
Kind: "every",
|
|
EveryMS: &everyMS,
|
|
}, "removal test", false, "", "", agentID.String(), "test-user")
|
|
if err != nil {
|
|
t.Fatalf("AddJob: %v", err)
|
|
}
|
|
jobUUID := uuid.MustParse(job.ID)
|
|
t.Cleanup(func() {
|
|
db.Exec("DELETE FROM cron_run_logs WHERE job_id = $1", jobUUID)
|
|
db.Exec("DELETE FROM cron_jobs WHERE id = $1", jobUUID)
|
|
})
|
|
|
|
if err := s.RemoveJob(ctx, job.ID); err != nil {
|
|
t.Fatalf("RemoveJob: %v", err)
|
|
}
|
|
|
|
// GetJob must return false after removal.
|
|
_, ok := s.GetJob(ctx, job.ID)
|
|
if ok {
|
|
t.Error("GetJob returned true after RemoveJob — row still present")
|
|
}
|
|
}
|
|
|
|
// TestStoreCron_EnableDisable verifies EnableJob toggles enabled flag and updates next_run_at.
|
|
func TestStoreCron_EnableDisable(t *testing.T) {
|
|
db := testDB(t)
|
|
tenantID, agentID := seedTenantAgent(t, db)
|
|
ctx := tenantCtx(tenantID)
|
|
s := newCronStore(t)
|
|
|
|
everyMS := int64(60000)
|
|
job, err := s.AddJob(ctx, "enable-disable-job", store.CronSchedule{
|
|
Kind: "every",
|
|
EveryMS: &everyMS,
|
|
}, "toggle test", false, "", "", agentID.String(), "test-user")
|
|
if err != nil {
|
|
t.Fatalf("AddJob: %v", err)
|
|
}
|
|
jobUUID := uuid.MustParse(job.ID)
|
|
t.Cleanup(func() {
|
|
db.Exec("DELETE FROM cron_run_logs WHERE job_id = $1", jobUUID)
|
|
db.Exec("DELETE FROM cron_jobs WHERE id = $1", jobUUID)
|
|
})
|
|
|
|
t.Run("disable", func(t *testing.T) {
|
|
if err := s.EnableJob(ctx, job.ID, false); err != nil {
|
|
t.Fatalf("EnableJob(false): %v", err)
|
|
}
|
|
got, ok := s.GetJob(ctx, job.ID)
|
|
if !ok {
|
|
t.Fatal("GetJob returned false")
|
|
}
|
|
if got.Enabled {
|
|
t.Error("expected Enabled=false after disable")
|
|
}
|
|
})
|
|
|
|
t.Run("re-enable", func(t *testing.T) {
|
|
if err := s.EnableJob(ctx, job.ID, true); err != nil {
|
|
t.Fatalf("EnableJob(true): %v", err)
|
|
}
|
|
got, ok := s.GetJob(ctx, job.ID)
|
|
if !ok {
|
|
t.Fatal("GetJob returned false")
|
|
}
|
|
if !got.Enabled {
|
|
t.Error("expected Enabled=true after re-enable")
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestStoreCron_UpdateJob_Name verifies UpdateJob patches the name field.
|
|
func TestStoreCron_UpdateJob_Name(t *testing.T) {
|
|
db := testDB(t)
|
|
tenantID, agentID := seedTenantAgent(t, db)
|
|
ctx := tenantCtx(tenantID)
|
|
s := newCronStore(t)
|
|
|
|
everyMS := int64(60000)
|
|
job, err := s.AddJob(ctx, "update-name-original", store.CronSchedule{
|
|
Kind: "every",
|
|
EveryMS: &everyMS,
|
|
}, "update test", false, "", "", agentID.String(), "test-user")
|
|
if err != nil {
|
|
t.Fatalf("AddJob: %v", err)
|
|
}
|
|
jobUUID := uuid.MustParse(job.ID)
|
|
t.Cleanup(func() {
|
|
db.Exec("DELETE FROM cron_run_logs WHERE job_id = $1", jobUUID)
|
|
db.Exec("DELETE FROM cron_jobs WHERE id = $1", jobUUID)
|
|
})
|
|
|
|
updated, err := s.UpdateJob(ctx, job.ID, store.CronJobPatch{Name: "update-name-new"})
|
|
if err != nil {
|
|
t.Fatalf("UpdateJob: %v", err)
|
|
}
|
|
if updated.Name != "update-name-new" {
|
|
t.Errorf("Name = %q, want %q", updated.Name, "update-name-new")
|
|
}
|
|
|
|
// Verify persisted.
|
|
got, ok := s.GetJob(ctx, job.ID)
|
|
if !ok {
|
|
t.Fatal("GetJob after update returned false")
|
|
}
|
|
if got.Name != "update-name-new" {
|
|
t.Errorf("persisted Name = %q, want %q", got.Name, "update-name-new")
|
|
}
|
|
}
|
|
|
|
// TestStoreCron_ListJobs_FilterByAgent verifies ListJobs agentID filter.
|
|
func TestStoreCron_ListJobs_FilterByAgent(t *testing.T) {
|
|
db := testDB(t)
|
|
tenantID, agentA := seedTenantAgent(t, db)
|
|
_, agentB := seedTenantAgent(t, db)
|
|
ctx := tenantCtx(tenantID)
|
|
s := newCronStore(t)
|
|
|
|
everyMS := int64(60000)
|
|
jobA, err := s.AddJob(ctx, "filter-agent-A-job", store.CronSchedule{
|
|
Kind: "every",
|
|
EveryMS: &everyMS,
|
|
}, "for agent A", false, "", "", agentA.String(), "user-a")
|
|
if err != nil {
|
|
t.Fatalf("AddJob agentA: %v", err)
|
|
}
|
|
|
|
jobB, err := s.AddJob(ctx, "filter-agent-B-job", store.CronSchedule{
|
|
Kind: "every",
|
|
EveryMS: &everyMS,
|
|
}, "for agent B", false, "", "", agentB.String(), "user-b")
|
|
if err != nil {
|
|
t.Fatalf("AddJob agentB: %v", err)
|
|
}
|
|
|
|
t.Cleanup(func() {
|
|
for _, id := range []string{jobA.ID, jobB.ID} {
|
|
u := uuid.MustParse(id)
|
|
db.Exec("DELETE FROM cron_run_logs WHERE job_id = $1", u)
|
|
db.Exec("DELETE FROM cron_jobs WHERE id = $1", u)
|
|
}
|
|
})
|
|
|
|
// Filter by agentA — must not include jobB.
|
|
listA := s.ListJobs(ctx, true, agentA.String(), "")
|
|
for _, j := range listA {
|
|
if j.ID == jobB.ID {
|
|
t.Error("ListJobs(agentA) returned jobB — filter broken")
|
|
}
|
|
}
|
|
foundA := false
|
|
for _, j := range listA {
|
|
if j.ID == jobA.ID {
|
|
foundA = true
|
|
}
|
|
}
|
|
if !foundA {
|
|
t.Error("ListJobs(agentA) did not return jobA")
|
|
}
|
|
}
|
|
|
|
// TestStoreCron_RunLog_MultipleEntries verifies run log accumulates correctly and pagination works.
|
|
func TestStoreCron_RunLog_MultipleStatuses(t *testing.T) {
|
|
db := testDB(t)
|
|
tenantID, agentID := seedTenantAgent(t, db)
|
|
ctx := tenantCtx(tenantID)
|
|
s := newCronStore(t)
|
|
|
|
everyMS := int64(60000)
|
|
job, err := s.AddJob(ctx, "multi-status-job", store.CronSchedule{
|
|
Kind: "every",
|
|
EveryMS: &everyMS,
|
|
}, "multi status", false, "", "", agentID.String(), "test-user")
|
|
if err != nil {
|
|
t.Fatalf("AddJob: %v", err)
|
|
}
|
|
jobUUID := uuid.MustParse(job.ID)
|
|
t.Cleanup(func() {
|
|
db.Exec("DELETE FROM cron_run_logs WHERE job_id = $1", jobUUID)
|
|
db.Exec("DELETE FROM cron_jobs WHERE id = $1", jobUUID)
|
|
})
|
|
|
|
statuses := []struct {
|
|
status string
|
|
summary string
|
|
}{
|
|
{"ok", "run succeeded"},
|
|
{"error", "something failed"},
|
|
{"ok", "run succeeded again"},
|
|
}
|
|
base := time.Now()
|
|
for i, row := range statuses {
|
|
_, insertErr := db.ExecContext(ctx,
|
|
`INSERT INTO cron_run_logs (id, job_id, agent_id, status, summary, duration_ms, input_tokens, output_tokens, ran_at)
|
|
VALUES ($1, $2, $3, $4, $5, 100, 10, 5, $6)`,
|
|
uuid.Must(uuid.NewV7()), jobUUID, agentID, row.status, row.summary,
|
|
base.Add(time.Duration(i)*time.Second),
|
|
)
|
|
if insertErr != nil {
|
|
t.Fatalf("insert run log %d: %v", i, insertErr)
|
|
}
|
|
}
|
|
|
|
entries, total := s.GetRunLog(ctx, job.ID, 10, 0)
|
|
if total != 3 {
|
|
t.Errorf("total = %d, want 3", total)
|
|
}
|
|
if len(entries) != 3 {
|
|
t.Errorf("entries len = %d, want 3", len(entries))
|
|
}
|
|
|
|
// Verify statuses are captured correctly (order DESC by ran_at).
|
|
statusSeen := map[string]int{}
|
|
for _, e := range entries {
|
|
statusSeen[e.Status]++
|
|
}
|
|
if statusSeen["ok"] != 2 {
|
|
t.Errorf("ok count = %d, want 2", statusSeen["ok"])
|
|
}
|
|
if statusSeen["error"] != 1 {
|
|
t.Errorf("error count = %d, want 1", statusSeen["error"])
|
|
}
|
|
}
|
|
|
|
// TestStoreCron_UpdateJob_Schedule verifies UpdateJob patches the schedule.
|
|
func TestStoreCron_UpdateJob_Schedule(t *testing.T) {
|
|
db := testDB(t)
|
|
tenantID, agentID := seedTenantAgent(t, db)
|
|
ctx := tenantCtx(tenantID)
|
|
s := newCronStore(t)
|
|
|
|
everyMS := int64(60000) // 60s
|
|
job, err := s.AddJob(ctx, "sched-update-job", store.CronSchedule{
|
|
Kind: "every",
|
|
EveryMS: &everyMS,
|
|
}, "schedule update", false, "", "", agentID.String(), "test-user")
|
|
if err != nil {
|
|
t.Fatalf("AddJob: %v", err)
|
|
}
|
|
jobUUID := uuid.MustParse(job.ID)
|
|
t.Cleanup(func() {
|
|
db.Exec("DELETE FROM cron_run_logs WHERE job_id = $1", jobUUID)
|
|
db.Exec("DELETE FROM cron_jobs WHERE id = $1", jobUUID)
|
|
})
|
|
|
|
newEveryMS := int64(120000) // 120s
|
|
newSched := &store.CronSchedule{
|
|
Kind: "every",
|
|
EveryMS: &newEveryMS,
|
|
}
|
|
updated, err := s.UpdateJob(ctx, job.ID, store.CronJobPatch{Schedule: newSched})
|
|
if err != nil {
|
|
t.Fatalf("UpdateJob schedule: %v", err)
|
|
}
|
|
if updated.Schedule.EveryMS == nil {
|
|
t.Fatal("updated schedule EveryMS is nil")
|
|
}
|
|
if *updated.Schedule.EveryMS != newEveryMS {
|
|
t.Errorf("EveryMS = %d, want %d", *updated.Schedule.EveryMS, newEveryMS)
|
|
}
|
|
}
|