From bebb46a1893e344e14065a6fb89d0cb271bb1367 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Flor=C3=AAncio?= Date: Wed, 17 Jun 2026 09:54:44 +0100 Subject: [PATCH] fix: support postgres initial agent setup (#1410) --- apps/backend/cmd/kandev/postgres_boot_test.go | 28 ++++++++++++ .../settings/store/postgres_schema_test.go | 43 +++++++++++++++++++ .../internal/agent/settings/store/sqlite.go | 7 ++- .../backend/internal/system/settings/store.go | 5 +++ .../internal/system/settings/store_test.go | 21 +++++++++ 5 files changed, 102 insertions(+), 2 deletions(-) diff --git a/apps/backend/cmd/kandev/postgres_boot_test.go b/apps/backend/cmd/kandev/postgres_boot_test.go index b6daa57ab8..fa0fccc20a 100644 --- a/apps/backend/cmd/kandev/postgres_boot_test.go +++ b/apps/backend/cmd/kandev/postgres_boot_test.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "net/url" "os" @@ -12,7 +13,9 @@ import ( _ "github.com/jackc/pgx/v5/stdlib" "github.com/jmoiron/sqlx" + "github.com/kandev/kandev/internal/agent/registry" "github.com/kandev/kandev/internal/common/config" + "github.com/kandev/kandev/internal/events/bus" "github.com/kandev/kandev/internal/testutil" ) @@ -62,6 +65,31 @@ func TestPostgresBootInitializesRepositories(t *testing.T) { } } }) + + agentRegistry, registryCleanup, err := registry.Provide(log) + if err != nil { + t.Fatalf("provide agent registry: %v", err) + } + t.Cleanup(func() { + if registryCleanup != nil { + _ = registryCleanup() + } + }) + services, agentSettingsController, err := provideServices( + cfg, + log, + repos, + pool, + bus.NewMemoryEventBus(log), + agentRegistry, + "test-postgres-boot", + ) + if err != nil { + t.Fatalf("provide services with postgres: %v", err) + } + if err := runInitialAgentSetup(context.Background(), services.User, agentSettingsController, log); err != nil { + t.Fatalf("run initial agent setup with postgres: %v", err) + } } type postgresConnInfo struct { diff --git a/apps/backend/internal/agent/settings/store/postgres_schema_test.go b/apps/backend/internal/agent/settings/store/postgres_schema_test.go index 800dd4c626..9d6c2e2c7a 100644 --- a/apps/backend/internal/agent/settings/store/postgres_schema_test.go +++ b/apps/backend/internal/agent/settings/store/postgres_schema_test.go @@ -1,6 +1,7 @@ package store import ( + "context" "testing" "github.com/kandev/kandev/internal/testutil" @@ -13,3 +14,45 @@ func TestPostgresFreshSchemaInitializes(t *testing.T) { t.Fatalf("init fresh postgres schema: %v", err) } } + +func TestPostgresHasDeletedAgentProfilesHandlesMissingAgent(t *testing.T) { + db := testutil.OpenIsolatedPostgres(t, testutil.PostgresDSNFromEnv(t)) + repo, err := newSQLiteRepositoryWithDB(db, db, nil) + if err != nil { + t.Fatalf("init fresh postgres schema: %v", err) + } + + hasDeleted, err := repo.HasDeletedAgentProfiles(context.Background(), "missing-agent") + if err != nil { + t.Fatalf("HasDeletedAgentProfiles: %v", err) + } + if hasDeleted { + t.Fatal("HasDeletedAgentProfiles = true, want false") + } +} + +func TestPostgresHasDeletedAgentProfilesReturnsTrueForDeletedProfile(t *testing.T) { + db := testutil.OpenIsolatedPostgres(t, testutil.PostgresDSNFromEnv(t)) + repo, err := newSQLiteRepositoryWithDB(db, db, nil) + if err != nil { + t.Fatalf("init fresh postgres schema: %v", err) + } + + ctx := context.Background() + profileID := seedAgentProfile(t, repo, "postgres-deleted", "postgres-agent") + profile, err := repo.GetAgentProfileIncludingDeleted(ctx, profileID) + if err != nil { + t.Fatalf("lookup profile: %v", err) + } + if err := repo.DeleteAgentProfile(ctx, profileID); err != nil { + t.Fatalf("delete profile: %v", err) + } + + hasDeleted, err := repo.HasDeletedAgentProfiles(ctx, profile.AgentID) + if err != nil { + t.Fatalf("HasDeletedAgentProfiles: %v", err) + } + if !hasDeleted { + t.Fatal("HasDeletedAgentProfiles = false, want true") + } +} diff --git a/apps/backend/internal/agent/settings/store/sqlite.go b/apps/backend/internal/agent/settings/store/sqlite.go index 69eb176389..9440a98580 100644 --- a/apps/backend/internal/agent/settings/store/sqlite.go +++ b/apps/backend/internal/agent/settings/store/sqlite.go @@ -860,12 +860,15 @@ func (r *sqliteRepository) ListAgentProfiles(ctx context.Context, agentID string func (r *sqliteRepository) HasDeletedAgentProfiles(ctx context.Context, agentID string) (bool, error) { var exists int err := r.ro.QueryRowContext(ctx, - r.ro.Rebind(`SELECT EXISTS(SELECT 1 FROM agent_profiles WHERE agent_id = ? AND deleted_at IS NOT NULL)`), + r.ro.Rebind(`SELECT 1 FROM agent_profiles WHERE agent_id = ? AND deleted_at IS NOT NULL LIMIT 1`), agentID).Scan(&exists) + if errors.Is(err, sql.ErrNoRows) { + return false, nil + } if err != nil { return false, err } - return exists == 1, nil + return true, nil } // applyLegacyBackfill returns the profile with CLIFlags populated from the diff --git a/apps/backend/internal/system/settings/store.go b/apps/backend/internal/system/settings/store.go index 31f7ae1799..c074979896 100644 --- a/apps/backend/internal/system/settings/store.go +++ b/apps/backend/internal/system/settings/store.go @@ -8,6 +8,7 @@ import ( "github.com/jmoiron/sqlx" "github.com/kandev/kandev/internal/db" + "github.com/kandev/kandev/internal/db/dialect" ) // Store persists install-wide Kandev settings that are not scoped to a user, @@ -39,6 +40,10 @@ func (s *Store) initSchema() error { } func (s *Store) migrateLegacySystemSettings() error { + if dialect.IsPostgres(s.db.DriverName()) { + return nil + } + var exists int if err := s.db.QueryRow(` SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = 'system_settings' diff --git a/apps/backend/internal/system/settings/store_test.go b/apps/backend/internal/system/settings/store_test.go index a6b5bc2133..2d7fd81014 100644 --- a/apps/backend/internal/system/settings/store_test.go +++ b/apps/backend/internal/system/settings/store_test.go @@ -8,6 +8,7 @@ import ( _ "github.com/mattn/go-sqlite3" "github.com/kandev/kandev/internal/db" + "github.com/kandev/kandev/internal/testutil" ) func TestStoreSaveAndGet(t *testing.T) { @@ -62,6 +63,26 @@ func TestStoreMigratesLegacySystemSettings(t *testing.T) { } } +func TestStoreInitializesOnPostgres(t *testing.T) { + conn := testutil.OpenIsolatedPostgres(t, testutil.PostgresDSNFromEnv(t)) + store, err := NewStore(db.NewPool(conn, conn)) + if err != nil { + t.Fatalf("new postgres store: %v", err) + } + + ctx := context.Background() + if err := store.Save(ctx, "system_metrics", []byte(`{"interval_seconds":5}`)); err != nil { + t.Fatalf("save postgres setting: %v", err) + } + value, found, err := store.Get(ctx, "system_metrics") + if err != nil { + t.Fatalf("get postgres setting: %v", err) + } + if !found || string(value) != `{"interval_seconds":5}` { + t.Fatalf("postgres value = (%q, %v)", value, found) + } +} + func newTestStore(t *testing.T) *Store { t.Helper() conn := newSQLite(t)