From 6df1ab49d844f82452bb57213195a796b64d81d4 Mon Sep 17 00:00:00 2001 From: Michael Gorbovitski Date: Wed, 17 Jun 2026 11:00:13 -0400 Subject: [PATCH] config: add -logs-dir flag to relocate logs.db out of the config dir logs.db is derived as a sibling of config.db (Join(configDirPath, "logs.db")), forcing the config and the large, churning request-log DB onto one directory. Add a -logs-dir flag (default from BIFROST_LOGS_DIR env, matching the -host/BIFROST_HOST precedence) threaded through Server.LogsDir -> LoadConfig. Applies to the default SQLite logs store; when the logs store is explicitly configured in config.json or is non-SQLite the override is ignored with a warning rather than silently. Adds a test for relocation and the empty-default case. Co-Authored-By: Claude Opus 4.6 --- transports/bifrost-http/lib/config.go | 53 +++- transports/bifrost-http/lib/config_test.go | 315 +++++++++++++-------- transports/bifrost-http/main.go | 3 + transports/bifrost-http/server/server.go | 5 +- 4 files changed, 246 insertions(+), 130 deletions(-) diff --git a/transports/bifrost-http/lib/config.go b/transports/bifrost-http/lib/config.go index 2b5887cc47..b56d027ff0 100644 --- a/transports/bifrost-http/lib/config.go +++ b/transports/bifrost-http/lib/config.go @@ -762,10 +762,28 @@ func registerFeatureFlags(_ context.Context) error { // - Case conversion for provider names (e.g., "OpenAI" -> "openai") // - In-memory storage for ultra-fast access during request processing // - Graceful handling of missing config files -func LoadConfig(ctx context.Context, configDirPath string) (*Config, error) { +// LoadConfig loads configuration from configDirPath. logsDir overrides where logs.db is placed: +// by default (empty) logs.db is a sibling of config.db, which forces config and the large, +// churning request-log DB onto the same mount; a non-empty logsDir puts logs.db in its own +// directory so the two can be bind-mounted independently. The directory is set via the -logs-dir +// flag / BIFROST_LOGS_DIR env (see transports/bifrost-http/main.go) and only applies to the +// default SQLite logstore — when the logstore is explicitly configured (config.json) or non-SQLite, +// logsDir is ignored and a warning is logged. +func LoadConfig(ctx context.Context, configDirPath, logsDir string) (*Config, error) { configFilePath := filepath.Join(configDirPath, "config.json") configDBPath := filepath.Join(configDirPath, "config.db") - logsDBPath := filepath.Join(configDirPath, "logs.db") + if logsDir == "" { + logsDir = configDirPath + } + // Ensure the logs dir exists; an operator-supplied -logs-dir may not exist yet, and SQLite + // won't create the parent directory — it would fail to open logs.db and block startup. + if logsDir != configDirPath { + if err := os.MkdirAll(logsDir, 0o755); err != nil { + return nil, fmt.Errorf("failed to create logs directory %s: %w", logsDir, err) + } + } + logsDBPath := filepath.Join(logsDir, "logs.db") + logsDirOverridden := logsDir != configDirPath // Initialize config config := &Config{ configPath: configFilePath, @@ -855,7 +873,7 @@ func LoadConfig(ctx context.Context, configDirPath string) (*Config, error) { // 1a. Vault config acknowledgement (initialization handled by enterprise layer) initVault(&configData) // 2. Stores (config, logs, vector) — creates defaults for absent configs - if err := initStores(ctx, config, &configData, configDBPath, logsDBPath); err != nil { + if err := initStores(ctx, config, &configData, configDBPath, logsDBPath, logsDirOverridden); err != nil { return nil, err } // 3. KV store @@ -922,7 +940,7 @@ func LoadConfig(ctx context.Context, configDirPath string) (*Config, error) { // initStores initializes config, logs, and vector stores. // When config data sections are absent (nil), creates default SQLite stores for persistence. -func initStores(ctx context.Context, config *Config, configData *ConfigData, configDBPath, logsDBPath string) error { +func initStores(ctx context.Context, config *Config, configData *ConfigData, configDBPath, logsDBPath string, logsDirOverridden bool) error { var err error // Initialize config store if configData.ConfigStoreConfig != nil && configData.ConfigStoreConfig.Enabled { @@ -957,7 +975,11 @@ func initStores(ctx context.Context, config *Config, configData *ConfigData, con // Initialize log store if configData.LogsStoreConfig != nil && configData.LogsStoreConfig.Enabled { - // Explicit logs store configuration from config.json + // Explicit logs store configuration from config.json — it wins over the -logs-dir/env + // override, so warn rather than silently ignore the requested relocation. + if logsDirOverridden { + logger.Warn("logs dir override ignored: logs store is explicitly configured in config.json (-logs-dir / BIFROST_LOGS_DIR only applies to the default SQLite logs store)") + } config.LogsStore, err = logstore.NewLogStore(ctx, configData.LogsStoreConfig, logger) if err != nil { return err @@ -977,6 +999,27 @@ func initStores(ctx context.Context, config *Config, configData *ConfigData, con return fmt.Errorf("failed to get logs store config: %w", dbErr) } } + // When a logs dir override is in effect, relocate the SQLite logs.db — but only when the + // stored path is the implicit default (next to config.db, or empty). A stored path that an + // operator explicitly configured takes precedence and is left unchanged (with a warning), + // so the override never silently switches an operator-chosen logs DB. logsDBPath already + // honors the override (see LoadConfig). Non-SQLite / unexpected payloads warn rather than + // silently no-op, so the operator isn't left wondering why logs.db didn't move. + defaultSQLiteLogsDBPath := filepath.Join(filepath.Dir(configDBPath), "logs.db") + if logsDirOverridden && logStoreConfig != nil { + if logStoreConfig.Type != logstore.LogStoreTypeSQLite { + logger.Warn("logs dir override ignored: stored logs store is %s, not SQLite (-logs-dir / BIFROST_LOGS_DIR only relocates the SQLite logs.db)", logStoreConfig.Type) + } else if sqliteConfig, ok := logStoreConfig.Config.(*logstore.SQLiteConfig); !ok { + logger.Warn("logs dir override ignored: stored SQLite logs store has an unexpected config payload; cannot relocate logs.db") + } else if sqliteConfig.Path == "" || sqliteConfig.Path == defaultSQLiteLogsDBPath { + if sqliteConfig.Path != logsDBPath { + logger.Info("logs dir override: relocating logs.db from stored path %s to %s", sqliteConfig.Path, logsDBPath) + sqliteConfig.Path = logsDBPath + } + } else if sqliteConfig.Path != logsDBPath { + logger.Warn("logs dir override ignored: stored SQLite path %s appears explicitly configured; keeping it unchanged", sqliteConfig.Path) + } + } if logStoreConfig == nil { logStoreConfig = &logstore.Config{ Enabled: true, diff --git a/transports/bifrost-http/lib/config_test.go b/transports/bifrost-http/lib/config_test.go index eccb201cd9..12f052898a 100644 --- a/transports/bifrost-http/lib/config_test.go +++ b/transports/bifrost-http/lib/config_test.go @@ -7920,7 +7920,7 @@ func TestSQLite_Provider_NewProviderFromFile(t *testing.T) { // Load config - this should create the provider in the DB ctx := context.Background() - config, err := LoadConfig(ctx, tempDir) + config, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("LoadConfig failed: %v", err) } @@ -7958,7 +7958,7 @@ func TestSQLite_Provider_HashMatch_DBPreserved(t *testing.T) { // First load - creates provider in DB ctx := context.Background() - config1, err := LoadConfig(ctx, tempDir) + config1, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("First LoadConfig failed: %v", err) } @@ -7969,7 +7969,7 @@ func TestSQLite_Provider_HashMatch_DBPreserved(t *testing.T) { config1.Close(ctx) // Second load with same config.json - should preserve DB config - config2, err := LoadConfig(ctx, tempDir) + config2, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("Second LoadConfig failed: %v", err) } @@ -8001,7 +8001,7 @@ func TestSQLite_Provider_HashMismatch_FileSync(t *testing.T) { // First load ctx := context.Background() - config1, err := LoadConfig(ctx, tempDir) + config1, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("First LoadConfig failed: %v", err) } @@ -8019,7 +8019,7 @@ func TestSQLite_Provider_HashMismatch_FileSync(t *testing.T) { createConfigFile(t, tempDir, configData2) // Second load with modified config.json - should sync from file - config2, err := LoadConfig(ctx, tempDir) + config2, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("Second LoadConfig failed: %v", err) } @@ -8053,7 +8053,7 @@ func TestSQLite_Provider_DBOnlyProvider_Preserved(t *testing.T) { // First load ctx := context.Background() - config1, err := LoadConfig(ctx, tempDir) + config1, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("First LoadConfig failed: %v", err) } @@ -8085,7 +8085,7 @@ func TestSQLite_Provider_DBOnlyProvider_Preserved(t *testing.T) { config1.Close(ctx) // Second load with same config.json (no Anthropic) - should preserve DB-added provider - config2, err := LoadConfig(ctx, tempDir) + config2, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("Second LoadConfig failed: %v", err) } @@ -8116,7 +8116,7 @@ func TestSQLite_SourceOfTruthConfigJSON_ProviderAndKeysPruned(t *testing.T) { createConfigFile(t, tempDir, configData) ctx := context.Background() - config1, err := LoadConfig(ctx, tempDir) + config1, err := LoadConfig(ctx, tempDir, "") require.NoError(t, err) existingProviders, err := config1.ConfigStore.GetProvidersConfig(ctx) @@ -8144,7 +8144,7 @@ func TestSQLite_SourceOfTruthConfigJSON_ProviderAndKeysPruned(t *testing.T) { configData.SourceOfTruth = SourceOfTruthConfigJSON createConfigFile(t, tempDir, configData) - config2, err := LoadConfig(ctx, tempDir) + config2, err := LoadConfig(ctx, tempDir, "") require.NoError(t, err) defer config2.Close(ctx) @@ -8171,7 +8171,7 @@ func TestSQLite_Provider_RoundTrip(t *testing.T) { // First load ctx := context.Background() - config1, err := LoadConfig(ctx, tempDir) + config1, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("First LoadConfig failed: %v", err) } @@ -8191,7 +8191,7 @@ func TestSQLite_Provider_RoundTrip(t *testing.T) { config1.Close(ctx) // Second load with same config.json - should preserve DB changes since hash matches - config2, err := LoadConfig(ctx, tempDir) + config2, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("Second LoadConfig failed: %v", err) } @@ -8233,7 +8233,7 @@ func TestSQLite_Key_NewKeyFromFile(t *testing.T) { // Load config ctx := context.Background() - config, err := LoadConfig(ctx, tempDir) + config, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("LoadConfig failed: %v", err) } @@ -8268,7 +8268,7 @@ func TestSQLite_Key_HashMatch_DBKeyPreserved(t *testing.T) { // First load ctx := context.Background() - config1, err := LoadConfig(ctx, tempDir) + config1, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("First LoadConfig failed: %v", err) } @@ -8279,7 +8279,7 @@ func TestSQLite_Key_HashMatch_DBKeyPreserved(t *testing.T) { config1.Close(ctx) // Second load with same config - config2, err := LoadConfig(ctx, tempDir) + config2, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("Second LoadConfig failed: %v", err) } @@ -8318,7 +8318,7 @@ func TestSQLite_Key_DashboardAddedKey_Preserved(t *testing.T) { // First load ctx := context.Background() - config1, err := LoadConfig(ctx, tempDir) + config1, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("First LoadConfig failed: %v", err) } @@ -8340,7 +8340,7 @@ func TestSQLite_Key_DashboardAddedKey_Preserved(t *testing.T) { config1.Close(ctx) // Second load with same config.json (still has only file-key) - config2, err := LoadConfig(ctx, tempDir) + config2, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("Second LoadConfig failed: %v", err) } @@ -8385,7 +8385,7 @@ func TestSQLite_Key_KeyValueChange_Detected(t *testing.T) { // First load ctx := context.Background() - config1, err := LoadConfig(ctx, tempDir) + config1, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("First LoadConfig failed: %v", err) } @@ -8410,7 +8410,7 @@ func TestSQLite_Key_KeyValueChange_Detected(t *testing.T) { createConfigFile(t, tempDir, configData2) // Second load with modified config - config2, err := LoadConfig(ctx, tempDir) + config2, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("Second LoadConfig failed: %v", err) } @@ -8445,7 +8445,7 @@ func TestSQLite_Key_MultipleKeys_MergeLogic(t *testing.T) { // First load ctx := context.Background() - config1, err := LoadConfig(ctx, tempDir) + config1, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("First LoadConfig failed: %v", err) } @@ -8472,7 +8472,7 @@ func TestSQLite_Key_MultipleKeys_MergeLogic(t *testing.T) { config1.Close(ctx) // Second load with same config.json (still has key-1 and key-2) - config2, err := LoadConfig(ctx, tempDir) + config2, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("Second LoadConfig failed: %v", err) } @@ -8515,7 +8515,7 @@ func TestSQLite_VirtualKey_NewFromFile(t *testing.T) { // Load config ctx := context.Background() - config, err := LoadConfig(ctx, tempDir) + config, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("LoadConfig failed: %v", err) } @@ -8555,7 +8555,7 @@ func TestSQLite_VirtualKey_HashMatch_DBPreserved(t *testing.T) { // First load ctx := context.Background() - config1, err := LoadConfig(ctx, tempDir) + config1, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("First LoadConfig failed: %v", err) } @@ -8566,7 +8566,7 @@ func TestSQLite_VirtualKey_HashMatch_DBPreserved(t *testing.T) { config1.Close(ctx) // Second load with same config.json - config2, err := LoadConfig(ctx, tempDir) + config2, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("Second LoadConfig failed: %v", err) } @@ -8596,7 +8596,7 @@ func TestSQLite_VirtualKey_HashMismatch_FileSync(t *testing.T) { // First load ctx := context.Background() - config1, err := LoadConfig(ctx, tempDir) + config1, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("First LoadConfig failed: %v", err) } @@ -8623,7 +8623,7 @@ func TestSQLite_VirtualKey_HashMismatch_FileSync(t *testing.T) { createConfigFile(t, tempDir, configData2) // Second load with modified config - config2, err := LoadConfig(ctx, tempDir) + config2, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("Second LoadConfig failed: %v", err) } @@ -8658,7 +8658,7 @@ func TestSQLite_VirtualKey_DBOnlyVK_Preserved(t *testing.T) { // First load ctx := context.Background() - config1, err := LoadConfig(ctx, tempDir) + config1, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("First LoadConfig failed: %v", err) } @@ -8680,7 +8680,7 @@ func TestSQLite_VirtualKey_DBOnlyVK_Preserved(t *testing.T) { config1.Close(ctx) // Second load with same config.json (only has vk-file) - config2, err := LoadConfig(ctx, tempDir) + config2, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("Second LoadConfig failed: %v", err) } @@ -8733,7 +8733,7 @@ func TestSQLite_VirtualKey_WithProviderConfigs(t *testing.T) { // Load config ctx := context.Background() - config, err := LoadConfig(ctx, tempDir) + config, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("LoadConfig failed: %v", err) } @@ -8794,7 +8794,7 @@ func TestSQLite_VirtualKey_MergePath_WithProviderConfigs(t *testing.T) { // First load - bootstrap path ctx := context.Background() - config1, err := LoadConfig(ctx, tempDir) + config1, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("First LoadConfig failed: %v", err) } @@ -8825,7 +8825,7 @@ func TestSQLite_VirtualKey_MergePath_WithProviderConfigs(t *testing.T) { createConfigFile(t, tempDir, configData2) // Second load - merge path (this is where the bug is) - config2, err := LoadConfig(ctx, tempDir) + config2, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("Second LoadConfig failed: %v", err) } @@ -8896,7 +8896,7 @@ func TestSQLite_VirtualKey_MergePath_WithProviderConfigKeys(t *testing.T) { // First load - bootstrap path (creates provider with key in DB) ctx := context.Background() - config1, err := LoadConfig(ctx, tempDir) + config1, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("First LoadConfig failed: %v", err) } @@ -8941,7 +8941,7 @@ func TestSQLite_VirtualKey_MergePath_WithProviderConfigKeys(t *testing.T) { // Second load - merge path // BEFORE FIX: This would fail because GORM tries to INSERT the key again // AFTER FIX: CreateVirtualKeyProviderConfig uses Append() to associate existing keys - config2, err := LoadConfig(ctx, tempDir) + config2, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("Second LoadConfig failed: %v", err) } @@ -9087,7 +9087,7 @@ func TestSQLite_VKProviderConfig_NewConfig(t *testing.T) { // Load config ctx := context.Background() - config, err := LoadConfig(ctx, tempDir) + config, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("LoadConfig failed: %v", err) } @@ -9159,7 +9159,7 @@ func TestSQLite_VKProviderConfig_KeyReference(t *testing.T) { // Load config ctx := context.Background() - config, err := LoadConfig(ctx, tempDir) + config, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("LoadConfig failed: %v", err) } @@ -9466,7 +9466,7 @@ func TestSQLite_FullLifecycle_InitialLoad(t *testing.T) { // Load config ctx := context.Background() - config, err := LoadConfig(ctx, tempDir) + config, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("LoadConfig failed: %v", err) } @@ -9525,7 +9525,7 @@ func TestSQLite_FullLifecycle_SecondLoadNoChanges(t *testing.T) { // First load ctx := context.Background() - config1, err := LoadConfig(ctx, tempDir) + config1, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("First LoadConfig failed: %v", err) } @@ -9538,7 +9538,7 @@ func TestSQLite_FullLifecycle_SecondLoadNoChanges(t *testing.T) { config1.Close(ctx) // Second load with same config.json - config2, err := LoadConfig(ctx, tempDir) + config2, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("Second LoadConfig failed: %v", err) } @@ -9580,7 +9580,7 @@ func TestSQLite_FullLifecycle_FileChange_Selective(t *testing.T) { // First load ctx := context.Background() - config1, err := LoadConfig(ctx, tempDir) + config1, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("First LoadConfig failed: %v", err) } @@ -9613,7 +9613,7 @@ func TestSQLite_FullLifecycle_FileChange_Selective(t *testing.T) { createConfigFile(t, tempDir, configData2) // Second load - config2, err := LoadConfig(ctx, tempDir) + config2, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("Second LoadConfig failed: %v", err) } @@ -9669,7 +9669,7 @@ func TestSQLite_FullLifecycle_DashboardEdits_ThenFileUnchanged(t *testing.T) { // First load ctx := context.Background() - config1, err := LoadConfig(ctx, tempDir) + config1, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("First LoadConfig failed: %v", err) } @@ -9707,7 +9707,7 @@ func TestSQLite_FullLifecycle_DashboardEdits_ThenFileUnchanged(t *testing.T) { config1.Close(ctx) // Second load with SAME config.json (unchanged) - config2, err := LoadConfig(ctx, tempDir) + config2, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("Second LoadConfig failed: %v", err) } @@ -9926,7 +9926,7 @@ func TestSQLite_VirtualKey_WithMCPConfigs(t *testing.T) { createConfigFile(t, tempDir, configData) // First load - creates VK - config1, err := LoadConfig(ctx, tempDir) + config1, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("First LoadConfig failed: %v", err) } @@ -10015,7 +10015,7 @@ func TestSQLite_VKMCPConfig_Reconciliation(t *testing.T) { createConfigFile(t, tempDir, configData) // First load - config1, err := LoadConfig(ctx, tempDir) + config1, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("First LoadConfig failed: %v", err) } @@ -10092,7 +10092,7 @@ func TestSQLite_VKMCPConfig_Reconciliation(t *testing.T) { createConfigFile(t, tempDir, configData2) // Second load - should trigger reconciliation - config2, err := LoadConfig(ctx, tempDir) + config2, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("Second LoadConfig failed: %v", err) } @@ -10189,7 +10189,7 @@ func TestSQLite_VirtualKey_DashboardProviderConfig_DeletedOnFileChange(t *testin createConfigFile(t, tempDir, configData) // Step 2: First load - bootstrap path - config1, err := LoadConfig(ctx, tempDir) + config1, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("First LoadConfig failed: %v", err) } @@ -10253,7 +10253,7 @@ func TestSQLite_VirtualKey_DashboardProviderConfig_DeletedOnFileChange(t *testin createConfigFile(t, tempDir, configData2) // Step 5: Second load - merge path with hash mismatch - config2, err := LoadConfig(ctx, tempDir) + config2, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("Second LoadConfig failed: %v", err) } @@ -10337,7 +10337,7 @@ func TestSQLite_VirtualKey_DashboardMCPConfig_DeletedOnFileChange(t *testing.T) createConfigFile(t, tempDir, configData) // Step 2: First load - bootstrap path - config1, err := LoadConfig(ctx, tempDir) + config1, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("First LoadConfig failed: %v", err) } @@ -10428,7 +10428,7 @@ func TestSQLite_VirtualKey_DashboardMCPConfig_DeletedOnFileChange(t *testing.T) createConfigFile(t, tempDir, configData2) // Step 5: Second load - merge path with hash mismatch - config2, err := LoadConfig(ctx, tempDir) + config2, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("Second LoadConfig failed: %v", err) } @@ -10507,7 +10507,7 @@ func TestSQLite_VKMCPConfig_AddRemove(t *testing.T) { createConfigFile(t, tempDir, configData) // First load - config1, err := LoadConfig(ctx, tempDir) + config1, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("First LoadConfig failed: %v", err) } @@ -10546,7 +10546,7 @@ func TestSQLite_VKMCPConfig_AddRemove(t *testing.T) { createConfigFile(t, tempDir, configData2) // Second load - should add MCP configs - config2, err := LoadConfig(ctx, tempDir) + config2, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("Second LoadConfig failed: %v", err) } @@ -10578,7 +10578,7 @@ func TestSQLite_VKMCPConfig_AddRemove(t *testing.T) { createConfigFile(t, tempDir, configData3) // Third load - mcpClient2 config should be DELETED (file is source of truth) - config3, err := LoadConfig(ctx, tempDir) + config3, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("Third LoadConfig failed: %v", err) } @@ -10629,7 +10629,7 @@ func TestSQLite_VKMCPConfig_UpdateTools(t *testing.T) { createConfigFile(t, tempDir, configData) // First load - config1, err := LoadConfig(ctx, tempDir) + config1, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("First LoadConfig failed: %v", err) } @@ -10679,7 +10679,7 @@ func TestSQLite_VKMCPConfig_UpdateTools(t *testing.T) { createConfigFile(t, tempDir, configData2) // Second load - should update tools - config2, err := LoadConfig(ctx, tempDir) + config2, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("Second LoadConfig failed: %v", err) } @@ -10723,7 +10723,7 @@ func TestSQLite_VK_ProviderAndMCPConfigs_Combined(t *testing.T) { createConfigFile(t, tempDir, configData) // First load to set up DB - config1, err := LoadConfig(ctx, tempDir) + config1, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("First LoadConfig failed: %v", err) } @@ -10761,7 +10761,7 @@ func TestSQLite_VK_ProviderAndMCPConfigs_Combined(t *testing.T) { createConfigFile(t, tempDir, configData2) // Load config - config2, err := LoadConfig(ctx, tempDir) + config2, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("LoadConfig failed: %v", err) } @@ -10856,7 +10856,7 @@ func TestSQLite_VKMCPConfig_MCPClientNameResolution(t *testing.T) { createConfigFile(t, tempDir, configData) // First load to set up MCP clients in DB - config1, err := LoadConfig(ctx, tempDir) + config1, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("First LoadConfig failed: %v", err) } @@ -10951,7 +10951,7 @@ func TestSQLite_VKMCPConfig_MCPClientNameResolution(t *testing.T) { } // Load config - this should resolve mcp_client_name to MCPClientID - config2, err := LoadConfig(ctx, tempDir) + config2, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("LoadConfig with mcp_client_name failed: %v", err) } @@ -11070,7 +11070,7 @@ func TestSQLite_VKMCPConfig_MCPClientNameNotFound(t *testing.T) { } // Load config - should not fail, but should skip the unresolvable MCP config - config, err := LoadConfig(ctx, tempDir) + config, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("LoadConfig should not fail when MCP client name is not found: %v", err) } @@ -11825,7 +11825,7 @@ func TestSQLite_Budget_NewFromFile(t *testing.T) { createConfigFile(t, tempDir, configData) ctx := context.Background() - config, err := LoadConfig(ctx, tempDir) + config, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("LoadConfig failed: %v", err) } @@ -11868,7 +11868,7 @@ func TestSQLite_Budget_HashMatch_DBPreserved(t *testing.T) { createConfigFile(t, tempDir, configData) ctx := context.Background() - config1, err := LoadConfig(ctx, tempDir) + config1, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("First LoadConfig failed: %v", err) } @@ -11879,7 +11879,7 @@ func TestSQLite_Budget_HashMatch_DBPreserved(t *testing.T) { config1.Close(ctx) // Second load - same config - config2, err := LoadConfig(ctx, tempDir) + config2, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("Second LoadConfig failed: %v", err) } @@ -11907,7 +11907,7 @@ func TestSQLite_Budget_HashMismatch_FileSync(t *testing.T) { createConfigFile(t, tempDir, configData) ctx := context.Background() - config1, err := LoadConfig(ctx, tempDir) + config1, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("First LoadConfig failed: %v", err) } @@ -11918,7 +11918,7 @@ func TestSQLite_Budget_HashMismatch_FileSync(t *testing.T) { createConfigFile(t, tempDir, configData) // Second load - should sync from file - config2, err := LoadConfig(ctx, tempDir) + config2, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("Second LoadConfig failed: %v", err) } @@ -11946,7 +11946,7 @@ func TestSQLite_Budget_DBOnly_Preserved(t *testing.T) { createConfigFile(t, tempDir, configData) ctx := context.Background() - config1, err := LoadConfig(ctx, tempDir) + config1, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("First LoadConfig failed: %v", err) } @@ -11963,7 +11963,7 @@ func TestSQLite_Budget_DBOnly_Preserved(t *testing.T) { config1.Close(ctx) // Reload - dashboard budget should be preserved - config2, err := LoadConfig(ctx, tempDir) + config2, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("Second LoadConfig failed: %v", err) } @@ -12097,7 +12097,7 @@ func TestSQLite_RateLimit_NewFromFile(t *testing.T) { createConfigFile(t, tempDir, configData) ctx := context.Background() - config, err := LoadConfig(ctx, tempDir) + config, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("LoadConfig failed: %v", err) } @@ -12135,7 +12135,7 @@ func TestSQLite_RateLimit_HashMismatch_FileSync(t *testing.T) { createConfigFile(t, tempDir, configData) ctx := context.Background() - config1, err := LoadConfig(ctx, tempDir) + config1, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("First LoadConfig failed: %v", err) } @@ -12147,7 +12147,7 @@ func TestSQLite_RateLimit_HashMismatch_FileSync(t *testing.T) { createConfigFile(t, tempDir, configData) // Second load - config2, err := LoadConfig(ctx, tempDir) + config2, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("Second LoadConfig failed: %v", err) } @@ -12275,7 +12275,7 @@ func TestSQLite_Customer_NewFromFile(t *testing.T) { createConfigFile(t, tempDir, configData) ctx := context.Background() - config, err := LoadConfig(ctx, tempDir) + config, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("LoadConfig failed: %v", err) } @@ -12309,7 +12309,7 @@ func TestSQLite_Customer_HashMismatch_FileSync(t *testing.T) { createConfigFile(t, tempDir, configData) ctx := context.Background() - config1, err := LoadConfig(ctx, tempDir) + config1, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("First LoadConfig failed: %v", err) } @@ -12319,7 +12319,7 @@ func TestSQLite_Customer_HashMismatch_FileSync(t *testing.T) { configData.Governance.Customers[0].Name = "Updated Customer" createConfigFile(t, tempDir, configData) - config2, err := LoadConfig(ctx, tempDir) + config2, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("Second LoadConfig failed: %v", err) } @@ -12450,7 +12450,7 @@ func TestSQLite_Team_NewFromFile(t *testing.T) { createConfigFile(t, tempDir, configData) ctx := context.Background() - config, err := LoadConfig(ctx, tempDir) + config, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("LoadConfig failed: %v", err) } @@ -12485,7 +12485,7 @@ func TestSQLite_Team_HashMismatch_FileSync(t *testing.T) { createConfigFile(t, tempDir, configData) ctx := context.Background() - config1, err := LoadConfig(ctx, tempDir) + config1, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("First LoadConfig failed: %v", err) } @@ -12495,7 +12495,7 @@ func TestSQLite_Team_HashMismatch_FileSync(t *testing.T) { configData.Governance.Teams[0].Name = "Updated Team" createConfigFile(t, tempDir, configData) - config2, err := LoadConfig(ctx, tempDir) + config2, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("Second LoadConfig failed: %v", err) } @@ -13403,7 +13403,7 @@ func TestSQLite_Governance_FullReconciliation(t *testing.T) { createConfigFile(t, tempDir, configData) ctx := context.Background() - config1, err := LoadConfig(ctx, tempDir) + config1, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("First LoadConfig failed: %v", err) } @@ -13433,7 +13433,7 @@ func TestSQLite_Governance_FullReconciliation(t *testing.T) { createConfigFile(t, tempDir, configData) // Reload and verify all entities are updated - config2, err := LoadConfig(ctx, tempDir) + config2, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("Second LoadConfig failed: %v", err) } @@ -13468,7 +13468,7 @@ func TestSQLite_Governance_DBOnly_AllPreserved(t *testing.T) { createConfigFile(t, tempDir, configData) ctx := context.Background() - config1, err := LoadConfig(ctx, tempDir) + config1, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("First LoadConfig failed: %v", err) } @@ -13504,7 +13504,7 @@ func TestSQLite_Governance_DBOnly_AllPreserved(t *testing.T) { config1.Close(ctx) // Reload - all dashboard entities should be preserved - config2, err := LoadConfig(ctx, tempDir) + config2, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("Second LoadConfig failed: %v", err) } @@ -13551,7 +13551,7 @@ func TestSQLite_SourceOfTruthConfigJSON_BulkEntityPruning(t *testing.T) { } createConfigFile(t, tempDir, configData) - config1, err := LoadConfig(ctx, tempDir) + config1, err := LoadConfig(ctx, tempDir, "") require.NoError(t, err) dbProviders, err := config1.ConfigStore.GetProvidersConfig(ctx) @@ -13590,7 +13590,7 @@ func TestSQLite_SourceOfTruthConfigJSON_BulkEntityPruning(t *testing.T) { configData.SourceOfTruth = SourceOfTruthConfigJSON createConfigFile(t, tempDir, configData) - config2, err := LoadConfig(ctx, tempDir) + config2, err := LoadConfig(ctx, tempDir, "") require.NoError(t, err) defer config2.Close(ctx) @@ -13927,7 +13927,7 @@ func TestSQLite_Governance_PricingOverrides_Reconciliation(t *testing.T) { ctx := context.Background() // First load: pricing override should be created in the DB - config1, err := LoadConfig(ctx, tempDir) + config1, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("First LoadConfig failed: %v", err) } @@ -13948,7 +13948,7 @@ func TestSQLite_Governance_PricingOverrides_Reconciliation(t *testing.T) { config1.Close(ctx) // Second load (unchanged config): should NOT fail with duplicate key error - config2, err := LoadConfig(ctx, tempDir) + config2, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("Second LoadConfig failed (duplicate key bug): %v", err) } @@ -13966,7 +13966,7 @@ func TestSQLite_Governance_PricingOverrides_Reconciliation(t *testing.T) { configData.Governance.PricingOverrides[0].Pattern = "gpt-4o" createConfigFile(t, tempDir, configData) - config3, err := LoadConfig(ctx, tempDir) + config3, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("Third LoadConfig failed: %v", err) } @@ -14884,7 +14884,7 @@ func TestKeyWeight_ZeroPreserved(t *testing.T) { configData := makeConfigDataWithProvidersAndDir(providers, tempDir) createConfigFile(t, tempDir, configData) - config, err := LoadConfig(context.Background(), tempDir) + config, err := LoadConfig(context.Background(), tempDir, "") if err != nil { t.Fatalf("LoadConfig failed: %v", err) } @@ -14921,7 +14921,7 @@ func TestKeyWeight_DefaultToOneWhenNotSet(t *testing.T) { configData := makeConfigDataWithProvidersAndDir(providers, tempDir) createConfigFile(t, tempDir, configData) - config, err := LoadConfig(context.Background(), tempDir) + config, err := LoadConfig(context.Background(), tempDir, "") if err != nil { t.Fatalf("LoadConfig failed: %v", err) } @@ -14957,7 +14957,7 @@ func TestSQLite_Key_WeightZero_RoundTrip(t *testing.T) { ctx := context.Background() // First load - creates DB entries - config1, err := LoadConfig(ctx, tempDir) + config1, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("First LoadConfig failed: %v", err) } @@ -14968,7 +14968,7 @@ func TestSQLite_Key_WeightZero_RoundTrip(t *testing.T) { config1.Close(ctx) // Second load - reads from DB - config2, err := LoadConfig(ctx, tempDir) + config2, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("Second LoadConfig failed: %v", err) } @@ -15014,7 +15014,7 @@ func TestVKProviderConfig_WeightZeroPreserved(t *testing.T) { createConfigFile(t, tempDir, configData) ctx := context.Background() - config, err := LoadConfig(ctx, tempDir) + config, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("LoadConfig failed: %v", err) } @@ -15076,7 +15076,7 @@ func TestSQLite_VKProviderConfig_WeightZero_RoundTrip(t *testing.T) { ctx := context.Background() // First load - config1, err := LoadConfig(ctx, tempDir) + config1, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("First LoadConfig failed: %v", err) } @@ -15094,7 +15094,7 @@ func TestSQLite_VKProviderConfig_WeightZero_RoundTrip(t *testing.T) { config1.Close(ctx) // Second load from DB - config2, err := LoadConfig(ctx, tempDir) + config2, err := LoadConfig(ctx, tempDir, "") if err != nil { t.Fatalf("Second LoadConfig failed: %v", err) } @@ -15271,7 +15271,7 @@ func TestSQLite_Key_EnabledChange_Detected(t *testing.T) { // First load createConfigFile(t, tempDir, initialConfig) - config1, err := LoadConfig(context.Background(), tempDir) + config1, err := LoadConfig(context.Background(), tempDir, "") if err != nil { t.Fatalf("First LoadConfig failed: %v", err) } @@ -15302,7 +15302,7 @@ func TestSQLite_Key_EnabledChange_Detected(t *testing.T) { // Second load with changed Enabled value createConfigFile(t, tempDir, updatedConfig) - config2, err := LoadConfig(context.Background(), tempDir) + config2, err := LoadConfig(context.Background(), tempDir, "") if err != nil { t.Fatalf("Second LoadConfig failed: %v", err) } @@ -15442,7 +15442,7 @@ func TestSQLite_Key_UseForBatchAPIChange_Detected(t *testing.T) { // First load createConfigFile(t, tempDir, initialConfig) - config1, err := LoadConfig(context.Background(), tempDir) + config1, err := LoadConfig(context.Background(), tempDir, "") if err != nil { t.Fatalf("First LoadConfig failed: %v", err) } @@ -15471,7 +15471,7 @@ func TestSQLite_Key_UseForBatchAPIChange_Detected(t *testing.T) { // Second load with changed UseForBatchAPI value createConfigFile(t, tempDir, updatedConfig) - config2, err := LoadConfig(context.Background(), tempDir) + config2, err := LoadConfig(context.Background(), tempDir, "") if err != nil { t.Fatalf("Second LoadConfig failed: %v", err) } @@ -15925,7 +15925,7 @@ func TestProviderHashComparison_VertexProviderFullLifecycle(t *testing.T) { }, tempDir) createConfigFile(t, tempDir, initialConfig) - config1, err := LoadConfig(context.Background(), tempDir) + config1, err := LoadConfig(context.Background(), tempDir, "") if err != nil { t.Fatalf("First LoadConfig failed: %v", err) } @@ -15961,7 +15961,7 @@ func TestProviderHashComparison_VertexProviderFullLifecycle(t *testing.T) { }, tempDir) createConfigFile(t, tempDir, updatedConfig) - config2, err := LoadConfig(context.Background(), tempDir) + config2, err := LoadConfig(context.Background(), tempDir, "") if err != nil { t.Fatalf("Second LoadConfig failed: %v", err) } @@ -15975,7 +15975,7 @@ func TestProviderHashComparison_VertexProviderFullLifecycle(t *testing.T) { } // Phase 3: Same config again - should not trigger update (hash matches) - config3, err := LoadConfig(context.Background(), tempDir) + config3, err := LoadConfig(context.Background(), tempDir, "") if err != nil { t.Fatalf("Third LoadConfig failed: %v", err) } @@ -16013,7 +16013,7 @@ func TestProviderHashComparison_VertexNewProviderFromConfig(t *testing.T) { }, tempDir) createConfigFile(t, tempDir, configData) - config, err := LoadConfig(context.Background(), tempDir) + config, err := LoadConfig(context.Background(), tempDir, "") if err != nil { t.Fatalf("LoadConfig failed: %v", err) } @@ -16063,7 +16063,7 @@ func TestProviderHashComparison_VertexDBValuePreservedWhenHashMatches(t *testing }, tempDir) createConfigFile(t, tempDir, configData) - config1, err := LoadConfig(context.Background(), tempDir) + config1, err := LoadConfig(context.Background(), tempDir, "") if err != nil { t.Fatalf("First LoadConfig failed: %v", err) } @@ -16076,7 +16076,7 @@ func TestProviderHashComparison_VertexDBValuePreservedWhenHashMatches(t *testing config1.Close(context.Background()) // Reload with same config file - DB value should be preserved since hash matches - config2, err := LoadConfig(context.Background(), tempDir) + config2, err := LoadConfig(context.Background(), tempDir, "") if err != nil { t.Fatalf("Second LoadConfig failed: %v", err) } @@ -16120,7 +16120,7 @@ func TestProviderHashComparison_VertexConfigChangedInFile(t *testing.T) { }, tempDir) createConfigFile(t, tempDir, initialConfig) - config1, err := LoadConfig(context.Background(), tempDir) + config1, err := LoadConfig(context.Background(), tempDir, "") if err != nil { t.Fatalf("First LoadConfig failed: %v", err) } @@ -16145,7 +16145,7 @@ func TestProviderHashComparison_VertexConfigChangedInFile(t *testing.T) { }, tempDir) createConfigFile(t, tempDir, updatedConfig) - config2, err := LoadConfig(context.Background(), tempDir) + config2, err := LoadConfig(context.Background(), tempDir, "") if err != nil { t.Fatalf("Second LoadConfig failed: %v", err) } @@ -18301,7 +18301,7 @@ func TestLoadConfig_NoConfigFile_FreshStart(t *testing.T) { tempDir := createTempDir(t) ctx := context.Background() - config, err := LoadConfig(ctx, tempDir) + config, err := LoadConfig(ctx, tempDir, "") require.NoError(t, err) require.NotNil(t, config) defer config.Close(ctx) @@ -18348,7 +18348,7 @@ func TestLoadConfig_NoConfigFile_ExistingDB(t *testing.T) { } createConfigFile(t, tempDir, configData) - config1, err := LoadConfig(ctx, tempDir) + config1, err := LoadConfig(ctx, tempDir, "") require.NoError(t, err) require.NotNil(t, config1) @@ -18362,7 +18362,7 @@ func TestLoadConfig_NoConfigFile_ExistingDB(t *testing.T) { require.NoError(t, os.Remove(filepath.Join(tempDir, "config.json"))) // Second run: no config.json, but DB has data - config2, err := LoadConfig(ctx, tempDir) + config2, err := LoadConfig(ctx, tempDir, "") require.NoError(t, err) require.NotNil(t, config2) defer config2.Close(ctx) @@ -18420,7 +18420,7 @@ func TestLoadConfig_FullConfigFile_FreshDB(t *testing.T) { createConfigFile(t, tempDir, configData) - config, err := LoadConfig(ctx, tempDir) + config, err := LoadConfig(ctx, tempDir, "") require.NoError(t, err) require.NotNil(t, config) defer config.Close(ctx) @@ -18459,7 +18459,7 @@ func TestLoadConfig_PartialConfigFile_OnlyProviders(t *testing.T) { createConfigFile(t, tempDir, configData) - config, err := LoadConfig(ctx, tempDir) + config, err := LoadConfig(ctx, tempDir, "") require.NoError(t, err) require.NotNil(t, config) defer config.Close(ctx) @@ -18495,7 +18495,7 @@ func TestLoadConfig_PartialConfigFile_OnlyClient(t *testing.T) { createConfigFile(t, tempDir, configData) - config, err := LoadConfig(ctx, tempDir) + config, err := LoadConfig(ctx, tempDir, "") require.NoError(t, err) require.NotNil(t, config) defer config.Close(ctx) @@ -18526,7 +18526,7 @@ func TestLoadConfig_PartialConfigFile_OnlyGovernance(t *testing.T) { createConfigFile(t, tempDir, configData) - config, err := LoadConfig(ctx, tempDir) + config, err := LoadConfig(ctx, tempDir, "") require.NoError(t, err) require.NotNil(t, config) defer config.Close(ctx) @@ -18554,7 +18554,7 @@ func TestLoadConfig_PartialConfigFile_OnlyPlugins(t *testing.T) { createConfigFile(t, tempDir, configData) - config, err := LoadConfig(ctx, tempDir) + config, err := LoadConfig(ctx, tempDir, "") require.NoError(t, err) require.NotNil(t, config) defer config.Close(ctx) @@ -18582,7 +18582,7 @@ func TestLoadConfig_PartialConfigFile_OnlyMCP(t *testing.T) { createConfigFile(t, tempDir, configData) - config, err := LoadConfig(ctx, tempDir) + config, err := LoadConfig(ctx, tempDir, "") require.NoError(t, err) require.NotNil(t, config) defer config.Close(ctx) @@ -18616,7 +18616,7 @@ func TestLoadConfig_PartialConfigFile_ClientAndProviders(t *testing.T) { createConfigFile(t, tempDir, configData) - config, err := LoadConfig(ctx, tempDir) + config, err := LoadConfig(ctx, tempDir, "") require.NoError(t, err) require.NotNil(t, config) defer config.Close(ctx) @@ -18643,7 +18643,7 @@ func TestLoadConfig_ConfigFile_NoConfigStoreSection(t *testing.T) { } createConfigFile(t, tempDir, configData) - config, err := LoadConfig(ctx, tempDir) + config, err := LoadConfig(ctx, tempDir, "") require.NoError(t, err) require.NotNil(t, config) defer config.Close(ctx) @@ -18673,7 +18673,7 @@ func TestLoadConfig_ConfigFile_ConfigStoreDisabled(t *testing.T) { } createConfigFile(t, tempDir, configData) - config, err := LoadConfig(ctx, tempDir) + config, err := LoadConfig(ctx, tempDir, "") require.NoError(t, err) require.NotNil(t, config) defer config.Close(ctx) @@ -18700,7 +18700,7 @@ func TestLoadConfig_NoConfigFile_SecondRun(t *testing.T) { } // First run: no config.json -> auto-detect and create defaults - config1, err := LoadConfig(ctx, tempDir) + config1, err := LoadConfig(ctx, tempDir, "") require.NoError(t, err) require.NotNil(t, config1) @@ -18715,7 +18715,7 @@ func TestLoadConfig_NoConfigFile_SecondRun(t *testing.T) { config1.Close(ctx) // Second run: still no config.json -> should load from DB - config2, err := LoadConfig(ctx, tempDir) + config2, err := LoadConfig(ctx, tempDir, "") require.NoError(t, err) require.NotNil(t, config2) defer config2.Close(ctx) @@ -18754,7 +18754,7 @@ func TestLoadConfig_PartialConfigFile_WithExistingDB(t *testing.T) { configData1 := makeConfigDataFullWithDir(nil, providers1, governance1, tempDir) createConfigFile(t, tempDir, configData1) - config1, err := LoadConfig(ctx, tempDir) + config1, err := LoadConfig(ctx, tempDir, "") require.NoError(t, err) require.Len(t, config1.Providers, 2, "Should have 2 providers from first load") require.NotNil(t, config1.GovernanceConfig) @@ -18772,7 +18772,7 @@ func TestLoadConfig_PartialConfigFile_WithExistingDB(t *testing.T) { // Note: no governance section in this config.json createConfigFile(t, tempDir, configData2) - config2, err := LoadConfig(ctx, tempDir) + config2, err := LoadConfig(ctx, tempDir, "") require.NoError(t, err) require.NotNil(t, config2) defer config2.Close(ctx) @@ -18798,7 +18798,7 @@ func TestLoadConfig_WebSocket_Defaults(t *testing.T) { configData := makeMinimalConfigData(tempDir) createConfigFile(t, tempDir, configData) - config, err := LoadConfig(ctx, tempDir) + config, err := LoadConfig(ctx, tempDir, "") require.NoError(t, err) require.NotNil(t, config) defer config.Close(ctx) @@ -18814,7 +18814,7 @@ func TestLoadConfig_DefaultClientConfig_Values(t *testing.T) { ctx := context.Background() // No config.json -> defaults applied - config, err := LoadConfig(ctx, tempDir) + config, err := LoadConfig(ctx, tempDir, "") require.NoError(t, err) require.NotNil(t, config) defer config.Close(ctx) @@ -18839,7 +18839,7 @@ func TestLoadConfig_PartialClientConfig_DefaultsFillGaps(t *testing.T) { createConfigFile(t, tempDir, configData) - config, err := LoadConfig(ctx, tempDir) + config, err := LoadConfig(ctx, tempDir, "") require.NoError(t, err) require.NotNil(t, config) defer config.Close(ctx) @@ -19196,7 +19196,7 @@ func TestVersionField_DefaultBehavior(t *testing.T) { } createConfigFile(t, tempDir, cd) - config, err := LoadConfig(ctx, tempDir) + config, err := LoadConfig(ctx, tempDir, "") require.NoError(t, err) require.NotNil(t, config) defer config.Close(ctx) @@ -19225,7 +19225,7 @@ func TestVersionField_Version1_AppliesCompat(t *testing.T) { } createConfigFile(t, tempDir, cd) - config, err := LoadConfig(ctx, tempDir) + config, err := LoadConfig(ctx, tempDir, "") require.NoError(t, err) require.NotNil(t, config) defer config.Close(ctx) @@ -19252,7 +19252,7 @@ func TestVersionField_Version2_NoCompat(t *testing.T) { } createConfigFile(t, tempDir, cd) - config, err := LoadConfig(ctx, tempDir) + config, err := LoadConfig(ctx, tempDir, "") require.NoError(t, err) require.NotNil(t, config) defer config.Close(ctx) @@ -19315,3 +19315,70 @@ func TestLoadPlugins_OtelPluginSpanFilterPassthrough(t *testing.T) { require.True(t, ok, "plugin_span_filter.plugins should be an array") require.ElementsMatch(t, []any{"logging", "compat"}, plugins) } + +// TestLoadConfig_LogsDirRelocatesLogsDB verifies the -logs-dir / BIFROST_LOGS_DIR knob: when a +// non-empty logsDir is passed, the default SQLite logs.db is created there (not alongside +// config.db in the config dir), so the two can live on independent mounts. An empty logsDir keeps +// logs.db in the config dir (historical default). +func TestLoadConfig_LogsDirRelocatesLogsDB(t *testing.T) { + initTestLogger() + ctx := context.Background() + + t.Run("separate logs dir", func(t *testing.T) { + configDir := createTempDir(t) + logsDir := createTempDir(t) + config, err := LoadConfig(ctx, configDir, logsDir) + require.NoError(t, err) + defer config.Close(ctx) + + require.FileExists(t, filepath.Join(logsDir, "logs.db"), "logs.db must be created in the override logs dir") + require.NoFileExists(t, filepath.Join(configDir, "logs.db"), "logs.db must NOT be created in the config dir when relocated") + require.FileExists(t, filepath.Join(configDir, "config.db"), "config.db must stay in the config dir") + }) + + t.Run("empty logs dir keeps default", func(t *testing.T) { + configDir := createTempDir(t) + config, err := LoadConfig(ctx, configDir, "") + require.NoError(t, err) + defer config.Close(ctx) + + require.FileExists(t, filepath.Join(configDir, "logs.db"), "logs.db must default to the config dir when no logs dir is set") + }) + + t.Run("explicit logs store config wins over logs dir override", func(t *testing.T) { + // An explicit logs_store in config.json takes precedence: the -logs-dir override is + // ignored (with a warning) and logs.db stays at the explicitly-configured path, NOT in + // the override dir. Guards the explicit-config Warn branch against silently relocating. + configDir := createTempDir(t) + logsDir := createTempDir(t) + explicitLogsDB := filepath.Join(configDir, "explicit-logs.db") + cd := makeConfigDataWithProvidersAndDir(map[string]configstore.ProviderConfig{}, configDir) + cd.LogsStoreConfig = &logstore.Config{ + Enabled: true, + Type: logstore.LogStoreTypeSQLite, + Config: &logstore.SQLiteConfig{Path: explicitLogsDB}, + } + createConfigFile(t, configDir, cd) + + config, err := LoadConfig(ctx, configDir, logsDir) + require.NoError(t, err) + defer config.Close(ctx) + + require.FileExists(t, explicitLogsDB, "explicit logs store path must be honored") + require.NoFileExists(t, filepath.Join(logsDir, "logs.db"), "logs dir override must be ignored when logs store is explicitly configured") + }) + + t.Run("logs dir created when it does not exist", func(t *testing.T) { + // An operator-supplied -logs-dir may not exist yet; LoadConfig must create it rather than + // crash on SQLite open (SQLite won't create the parent directory). + configDir := createTempDir(t) + logsDir := filepath.Join(createTempDir(t), "nested", "logs") + require.NoDirExists(t, logsDir) + + config, err := LoadConfig(ctx, configDir, logsDir) + require.NoError(t, err) + defer config.Close(ctx) + + require.FileExists(t, filepath.Join(logsDir, "logs.db"), "logs.db must be created in the auto-created logs dir") + }) +} diff --git a/transports/bifrost-http/main.go b/transports/bifrost-http/main.go index a53f10109d..1337495b4e 100644 --- a/transports/bifrost-http/main.go +++ b/transports/bifrost-http/main.go @@ -100,12 +100,15 @@ func init() { if defaultLogLevel == "" { defaultLogLevel = bifrostServer.DefaultLogLevel } + // Default logs dir from env (flag overrides); empty means logs.db sits alongside config.db. + defaultLogsDir := os.Getenv("BIFROST_LOGS_DIR") // Initializing server server = bifrostServer.NewBifrostHTTPServer(Version, uiContent) // Updating server properties from flags flag.StringVar(&server.Port, "port", bifrostServer.DefaultPort, "Port to run the server on") flag.StringVar(&server.Host, "host", defaultHost, "Host to bind the server to (default: localhost, override with BIFROST_HOST env var)") flag.StringVar(&server.AppDir, "app-dir", bifrostServer.DefaultAppDir, "Application data directory (contains config.json and logs)") + flag.StringVar(&server.LogsDir, "logs-dir", defaultLogsDir, "Directory for logs.db (default SQLite logs store only); defaults to BIFROST_LOGS_DIR, else alongside config.json in -app-dir") flag.StringVar(&server.LogLevel, "log-level", defaultLogLevel, "Logger level (debug, info, warn, error). Default is info.") flag.StringVar(&server.LogOutputStyle, "log-style", bifrostServer.DefaultLogOutputStyle, "Logger output type (json or pretty). Default is JSON.") } diff --git a/transports/bifrost-http/server/server.go b/transports/bifrost-http/server/server.go index 223b9fddec..075cf72909 100644 --- a/transports/bifrost-http/server/server.go +++ b/transports/bifrost-http/server/server.go @@ -138,6 +138,9 @@ type BifrostHTTPServer struct { Port string Host string AppDir string + // LogsDir, when set, places logs.db here instead of alongside config.db in AppDir + // (default SQLite logs store only). Set via -logs-dir / BIFROST_LOGS_DIR. + LogsDir string LogLevel string LogOutputStyle string @@ -1584,7 +1587,7 @@ func (s *BifrostHTTPServer) Bootstrap(ctx context.Context) error { return fmt.Errorf("failed to create app directory %s: %v", configDir, err) } // Initialize high-performance configuration store with dedicated database - s.Config, err = lib.LoadConfig(ctx, configDir) + s.Config, err = lib.LoadConfig(ctx, configDir, s.LogsDir) if err != nil { return fmt.Errorf("failed to load config %v", err) }