Repository: bacalhau-project/bacalhau (language: python)

Issue:
The `--repo` and `--data-dir` flag don't expand paths
### Bug Description
Running the command `bacalhau --repo=~/b_requester <command>` results in a config with a `DataDir` value of `~/b_requester` rather than an expanded path, e.g. `/home/$USER/b_requester`


Discussion / hints:

Fix patch:
diff --git a/CNAME b/CNAME
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/cmd/cli/config/set.go b/cmd/cli/config/set.go
index 28655c19ca..30ededb9a1 100644
--- a/cmd/cli/config/set.go
+++ b/cmd/cli/config/set.go
@@ -72,7 +72,7 @@ func newSetCmd() *cobra.Command {
 				value = args[1:]
 			}
 
-			return setConfig(cmd, configPath, key, value...)
+			return setConfig(cmd, rawConfig, configPath, key, value...)
 		},
 		// Provide auto completion for arguments to the `set` command
 		ValidArgsFunction: setAutoComplete,
@@ -82,7 +82,7 @@ func newSetCmd() *cobra.Command {
 	return setCmd
 }
 
-func setConfig(cmd *cobra.Command, cfgFilePath, key string, value ...string) error {
+func setConfig(cmd *cobra.Command, cfg *config.Config, cfgFilePath, key string, value ...string) error {
 	cmd.Printf("Writing config to %s", cfgFilePath)
 	v := viper.New()
 	v.SetConfigFile(cfgFilePath)
@@ -95,7 +95,15 @@ func setConfig(cmd *cobra.Command, cfgFilePath, key string, value ...string) err
 	if err != nil {
 		return err
 	}
-	v.Set(key, parsed)
+	if key == types.DataDirKey {
+		dataDirPath := config.AbsPathSilent(parsed.(string))
+		if err = config.ValidatePath(dataDirPath); err != nil {
+			return err
+		}
+		v.Set(key, dataDirPath)
+	} else {
+		v.Set(key, parsed)
+	}
 	if err := v.WriteConfigAs(cfgFilePath); err != nil {
 		return err
 	}
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 64627eb2ea..0d04a00f7d 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -7,6 +7,7 @@ import (
 	"path/filepath"
 	"strings"
 
+	"github.com/mitchellh/go-homedir"
 	"github.com/mitchellh/mapstructure"
 	"github.com/rs/zerolog/log"
 	"github.com/samber/lo"
@@ -15,6 +16,7 @@ import (
 
 	"github.com/bacalhau-project/bacalhau/pkg/bacerrors"
 	"github.com/bacalhau-project/bacalhau/pkg/config/types"
+	"github.com/bacalhau-project/bacalhau/pkg/models"
 	"github.com/bacalhau-project/bacalhau/pkg/util/idgen"
 )
 
@@ -22,6 +24,8 @@ const (
 	environmentVariablePrefix = "BACALHAU"
 	inferConfigTypes          = true
 	DefaultFileName           = "config.yaml"
+
+	errComponent = "config"
 )
 
 var (
@@ -116,23 +120,13 @@ func New(opts ...Option) (*Config, error) {
 
 	// To absolute paths for better logging. This is best effort and will not return an error if it fails.
 	for i, path := range c.paths {
-		if !filepath.IsAbs(path) {
-			absPath, err := filepath.Abs(path)
-			if err != nil {
-				log.Debug().Msgf("failed to resolve absolute path for %s: %v", path, err)
-			} else {
-				c.paths[i] = absPath
-			}
-		}
+		c.paths[i] = AbsPathSilent(path)
 	}
 
 	// merge the config files in the order they were passed.
 	for _, path := range c.paths {
-		if err := c.Merge(path); err != nil {
-			if os.IsNotExist(err) {
-				return nil, fmt.Errorf("the specified configuration file %q doesn't exist", path)
-			}
-			return nil, fmt.Errorf("opening config file %q: %w", path, err)
+		if err := c.merge(path); err != nil {
+			return nil, err
 		}
 	}
 
@@ -181,6 +175,10 @@ func New(opts ...Option) (*Config, error) {
 							c.values[types.ComputeEnabledKey] = true
 						}
 					}
+				case types.DataDirKey:
+					// Handle relative paths for data-dir flag
+					path := AbsPathSilent(flag.Value.String())
+					c.base.Set(types.DataDirKey, path)
 				case "default.publisher.deprecated":
 					// allow the deprecated --default-publisher flag to bind to related fields in the config.
 					for _, key := range []string{
@@ -202,10 +200,26 @@ func New(opts ...Option) (*Config, error) {
 	}
 
 	// merge the passed values last as they take highest precedence
+	// allow the user to set datadir as relative paths and resolve them to absolute paths
 	for name, value := range c.values {
+		if name == types.DataDirKey {
+			if val, ok := value.(string); ok {
+				value = AbsPathSilent(val)
+			}
+		}
 		c.base.Set(name, value)
 	}
 
+	// allow the users to set datadir to a path like ~/.bacalhau or ~/something/idk/whatever
+	// and expand the path for them
+	if expandedPath, err := homedir.Expand(c.base.GetString(types.DataDirKey)); err == nil {
+		c.base.Set(types.DataDirKey, expandedPath)
+	}
+
+	if err := ValidatePath(c.base.GetString(types.DataDirKey)); err != nil {
+		return nil, err
+	}
+
 	// if no config file was provided, we look for a config.yaml under the resolved data directory,
 	// and if it exists, we create and return a new config with the resolved path.
 	// we attempt this last to ensure the data-dir is resolved correctly from all config sources.
@@ -255,12 +269,15 @@ func (c *Config) Load(path string) error {
 	return nil
 }
 
-// Merge merges a new configuration file specified by `path` with the existing config.
-// Merge returns an error if the file cannot be read
-func (c *Config) Merge(path string) error {
+// merge merges a new configuration file specified by `path` with the existing config.
+// merge returns an error if the file cannot be read
+func (c *Config) merge(path string) error {
 	c.base.SetConfigFile(path)
 	if err := c.base.MergeInConfig(); err != nil {
-		return err
+		if os.IsNotExist(err) {
+			return fmt.Errorf("the specified configuration file %q doesn't exist", path)
+		}
+		return fmt.Errorf("opening config file %q: %w", path, err)
 	}
 	return nil
 }
@@ -285,6 +302,9 @@ func (c *Config) Unmarshal(out interface{}) error {
 	if err := c.base.Unmarshal(&out, DecoderHook); err != nil {
 		return err
 	}
+	if v, ok := out.(models.Validatable); ok {
+		return v.Validate()
+	}
 	return nil
 }
 
@@ -317,6 +337,44 @@ func GenerateNodeID(ctx context.Context, nodeNameProviderType string) (string, e
 	return nodeName, nil
 }
 
+func AbsPathSilent(path string) string {
+	expandedPath, err := homedir.Expand(path)
+	if err != nil {
+		log.Debug().Msgf("failed to expand path %s: %v", path, err)
+		return path
+	}
+	absPath, err := filepath.Abs(expandedPath)
+	if err != nil {
+		log.Debug().Msgf("failed to resolve absolute path for %s: %v", path, err)
+		return path
+	}
+	return absPath
+}
+
+func ValidatePath(path string) error {
+	if path == "" {
+		return bacerrors.New("data dir path is empty").
+			WithHint("Provide a valid path for the data directory").
+			WithComponent(errComponent).
+			WithCode(bacerrors.ValidationError)
+	}
+	if strings.Contains(path, "$") {
+		return bacerrors.New("data dir path %q contains a '$' character", path).
+			WithHint("Note that environment variables are not expanded will be used as-is").
+			WithComponent(errComponent).
+			WithCode(bacerrors.ValidationError)
+	}
+
+	if !filepath.IsAbs(path) {
+		return bacerrors.New("data dir path %q is not an absolute path", path).
+			WithHint("Use an absolute path for the data directory").
+			WithComponent(errComponent).
+			WithCode(bacerrors.ValidationError)
+	}
+
+	return nil
+}
+
 // checkFlagConfigConflicts checks for conflicts between cli flags and config values.
 // e.g. bacalhau serve --config=api.host=0.0.0.0 --api-host=0.0.0.0 should be rejected.
 func checkFlagConfigConflicts(flags map[string][]*pflag.Flag, cfgValues map[string]any) error {
diff --git a/pkg/config/types/default_config.go b/pkg/config/types/default_config.go
index 9a4f6d5e91..a9ae9d7235 100644
--- a/pkg/config/types/default_config.go
+++ b/pkg/config/types/default_config.go
@@ -130,17 +130,12 @@ const defaultBacalhauDir = ".bacalhau"
 
 // DefaultDataDir determines the appropriate default directory for storing repository data.
 // Priority order:
-// 1. If the environment variable BACALHAU_DIR is set and non-empty, use it.
-// 2. User's home directory with .bacalhau appended.
-// 3. If all above fail, use .bacalhau in the current directory.
+// 1. User's home directory with .bacalhau appended.
 func DefaultDataDir() string {
-	if repoDir, set := os.LookupEnv("BACALHAU_DIR"); set && repoDir != "" {
-		return repoDir
+	if userHome, err := os.UserHomeDir(); err == nil && userHome != "" {
+		if expandedUserHome, err := filepath.Abs(userHome); err == nil {
+			return filepath.Join(expandedUserHome, defaultBacalhauDir)
+		}
 	}
-
-	if userHome, err := os.UserHomeDir(); err == nil {
-		return filepath.Join(userHome, defaultBacalhauDir)
-	}
-
-	return defaultBacalhauDir
+	return ""
 }
diff --git a/pkg/config/types/paths.go b/pkg/config/types/paths.go
index 2fff193073..99d7e95499 100644
--- a/pkg/config/types/paths.go
+++ b/pkg/config/types/paths.go
@@ -7,11 +7,11 @@ import (
 
 const UserKeyFileName = "user_id.pem"
 
-func (c Bacalhau) UserKeyPath() (string, error) {
-	if c.DataDir == "" {
+func (b Bacalhau) UserKeyPath() (string, error) {
+	if b.DataDir == "" {
 		return "", fmt.Errorf("data dir not set")
 	}
-	path := filepath.Join(c.DataDir, UserKeyFileName)
+	path := filepath.Join(b.DataDir, UserKeyFileName)
 	if exists, err := fileExists(path); err != nil {
 		return "", fmt.Errorf("checking if user key exists: %w", err)
 	} else if exists {
@@ -25,20 +25,20 @@ func (c Bacalhau) UserKeyPath() (string, error) {
 
 const AuthTokensFileName = "tokens.json"
 
-func (c Bacalhau) AuthTokensPath() (string, error) {
-	if c.DataDir == "" {
+func (b Bacalhau) AuthTokensPath() (string, error) {
+	if b.DataDir == "" {
 		return "", fmt.Errorf("data dir not set")
 	}
-	return filepath.Join(c.DataDir, AuthTokensFileName), nil
+	return filepath.Join(b.DataDir, AuthTokensFileName), nil
 }
 
 const OrchestratorDirName = "orchestrator"
 
-func (c Bacalhau) OrchestratorDir() (string, error) {
-	if c.DataDir == "" {
+func (b Bacalhau) OrchestratorDir() (string, error) {
+	if b.DataDir == "" {
 		return "", fmt.Errorf("data dir not set")
 	}
-	path := filepath.Join(c.DataDir, OrchestratorDirName)
+	path := filepath.Join(b.DataDir, OrchestratorDirName)
 	if err := ensureDir(path); err != nil {
 		return "", fmt.Errorf("getting orchestrator path: %w", err)
 	}
@@ -47,24 +47,24 @@ func (c Bacalhau) OrchestratorDir() (string, error) {
 
 const JobStoreFileName = "state_boltdb.db"
 
-func (c Bacalhau) JobStoreFilePath() (string, error) {
-	if c.DataDir == "" {
+func (b Bacalhau) JobStoreFilePath() (string, error) {
+	if b.DataDir == "" {
 		return "", fmt.Errorf("data dir not set")
 	}
 	// make sure the parent dir exists first
-	if _, err := c.OrchestratorDir(); err != nil {
+	if _, err := b.OrchestratorDir(); err != nil {
 		return "", fmt.Errorf("getting job store path: %w", err)
 	}
-	return filepath.Join(c.DataDir, OrchestratorDirName, JobStoreFileName), nil
+	return filepath.Join(b.DataDir, OrchestratorDirName, JobStoreFileName), nil
 }
 
 const NetworkTransportDirName = "nats-store"
 
-func (c Bacalhau) NetworkTransportDir() (string, error) {
-	if c.DataDir == "" {
+func (b Bacalhau) NetworkTransportDir() (string, error) {
+	if b.DataDir == "" {
 		return "", fmt.Errorf("data dir not set")
 	}
-	path := filepath.Join(c.DataDir, OrchestratorDirName, NetworkTransportDirName)
+	path := filepath.Join(b.DataDir, OrchestratorDirName, NetworkTransportDirName)
 	if err := ensureDir(path); err != nil {
 		return "", fmt.Errorf("getting network transport path: %w", err)
 	}
@@ -73,11 +73,11 @@ func (c Bacalhau) NetworkTransportDir() (string, error) {
 
 const ComputeDirName = "compute"
 
-func (c Bacalhau) ComputeDir() (string, error) {
-	if c.DataDir == "" {
+func (b Bacalhau) ComputeDir() (string, error) {
+	if b.DataDir == "" {
 		return "", fmt.Errorf("data dir not set")
 	}
-	path := filepath.Join(c.DataDir, ComputeDirName)
+	path := filepath.Join(b.DataDir, ComputeDirName)
 	if err := ensureDir(path); err != nil {
 		return "", fmt.Errorf("getting compute path: %w", err)
 	}
@@ -86,11 +86,11 @@ func (c Bacalhau) ComputeDir() (string, error) {
 
 const ExecutionDirName = "executions"
 
-func (c Bacalhau) ExecutionDir() (string, error) {
-	if c.DataDir == "" {
+func (b Bacalhau) ExecutionDir() (string, error) {
+	if b.DataDir == "" {
 		return "", fmt.Errorf("data dir not set")
 	}
-	path := filepath.Join(c.DataDir, ComputeDirName, ExecutionDirName)
+	path := filepath.Join(b.DataDir, ComputeDirName, ExecutionDirName)
 	if err := ensureDir(path); err != nil {
 		return "", fmt.Errorf("getting executions path: %w", err)
 	}
@@ -99,11 +99,11 @@ func (c Bacalhau) ExecutionDir() (string, error) {
 
 const PluginsDirName = "plugins"
 
-func (c Bacalhau) PluginsDir() (string, error) {
-	if c.DataDir == "" {
+func (b Bacalhau) PluginsDir() (string, error) {
+	if b.DataDir == "" {
 		return "", fmt.Errorf("data dir not set")
 	}
-	path := filepath.Join(c.DataDir, PluginsDirName)
+	path := filepath.Join(b.DataDir, PluginsDirName)
 	if err := ensureDir(path); err != nil {
 		return "", fmt.Errorf("getting plugins path: %w", err)
 	}
@@ -112,12 +112,12 @@ func (c Bacalhau) PluginsDir() (string, error) {
 
 const ExecutionStoreFileName = "state_boltdb.db"
 
-func (c Bacalhau) ExecutionStoreFilePath() (string, error) {
-	if c.DataDir == "" {
+func (b Bacalhau) ExecutionStoreFilePath() (string, error) {
+	if b.DataDir == "" {
 		return "", fmt.Errorf("data dir not set")
 	}
-	if _, err := c.ComputeDir(); err != nil {
+	if _, err := b.ComputeDir(); err != nil {
 		return "", fmt.Errorf("getting execution store path: %w", err)
 	}
-	return filepath.Join(c.DataDir, ComputeDirName, ExecutionStoreFileName), nil
+	return filepath.Join(b.DataDir, ComputeDirName, ExecutionStoreFileName), nil
 }
diff --git a/pkg/repo/fs.go b/pkg/repo/fs.go
index 3eb2f37323..4e7724bcbe 100644
--- a/pkg/repo/fs.go
+++ b/pkg/repo/fs.go
@@ -2,6 +2,7 @@ package repo
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"os"
 	"path/filepath"
@@ -9,6 +10,8 @@ import (
 	"github.com/mitchellh/go-homedir"
 	"github.com/rs/zerolog/log"
 
+	"github.com/bacalhau-project/bacalhau/pkg/bacerrors"
+	"github.com/bacalhau-project/bacalhau/pkg/config"
 	"github.com/bacalhau-project/bacalhau/pkg/config_legacy"
 	legacy_types "github.com/bacalhau-project/bacalhau/pkg/config_legacy/types"
 	"github.com/bacalhau-project/bacalhau/pkg/telemetry"
@@ -92,7 +95,11 @@ func (fsr *FsRepo) Init() error {
 
 	// 0755: Owner can read, write, execute. Others can read and execute.
 	if err := os.MkdirAll(fsr.path, repoPermission); err != nil && !os.IsExist(err) {
-		return err
+		return bacerrors.New("failed to initialize the bacalhau repo at %q: %s", fsr.path, errors.Unwrap(err)).
+			WithHint("The data dir you've configured bacalhau to use is invalid\n"+
+				"\tIf provided, ensure the --data-dir/--repo flag contains a valid path\n"+
+				"\tIf present, ensure the config file provided by the --config flag contains a valid DataDir field path\n"+
+				"\tIf present, ensure the config file in %s contains a valid DataDir field path", filepath.Join(fsr.path, config.DefaultFileName))
 	}
 
 	// TODO this should be a part of the config.


Test patch:
diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go
index ca7ce120fb..252e9868db 100644
--- a/pkg/config/config_test.go
+++ b/pkg/config/config_test.go
@@ -3,6 +3,7 @@
 package config_test
 
 import (
+	"fmt"
 	"os"
 	"path/filepath"
 	"testing"
@@ -15,14 +16,185 @@ import (
 	"github.com/bacalhau-project/bacalhau/pkg/config/types"
 )
 
+func TestConfigDataDirPath(t *testing.T) {
+	workingDir, err := os.Getwd()
+	require.NoError(t, err)
+	homeDir := t.TempDir()
+	t.Setenv("HOME", homeDir)
+
+	testCases := []struct {
+		name          string
+		dataDir       string
+		setMethod     string // "cli", "config", "env", "file", "default"
+		isValid       bool
+		expected      string
+		errorContains string
+	}{
+		// CLI flag cases
+		{
+			name:      "CLI: Valid relative path",
+			dataDir:   "relative/path",
+			setMethod: "cli",
+			isValid:   true,
+			expected:  filepath.Join(workingDir, "relative/path"),
+		},
+		{
+			name:      "CLI: Valid tilde path",
+			dataDir:   "~/some/path",
+			setMethod: "cli",
+			isValid:   true,
+			expected:  filepath.Join(homeDir, "some/path"),
+		},
+		{
+			name:      "CLI: Valid absolute path",
+			dataDir:   "/absolute/path",
+			setMethod: "cli",
+			isValid:   true,
+			expected:  "/absolute/path",
+		},
+		// Config value cases
+		{
+			name:      "Config: Valid relative path",
+			dataDir:   "relative/path",
+			setMethod: "config",
+			isValid:   true,
+			expected:  filepath.Join(workingDir, "relative/path"),
+		},
+		{
+			name:      "Config: Valid tilde path",
+			dataDir:   "~/some/path",
+			setMethod: "config",
+			isValid:   true,
+			expected:  filepath.Join(homeDir, "some/path"),
+		},
+		{
+			name:      "Config: Valid absolute path",
+			dataDir:   "/absolute/path",
+			setMethod: "config",
+			isValid:   true,
+			expected:  "/absolute/path",
+		},
+		// Environment variable cases
+		{
+			name:          "Env: Invalid relative path",
+			dataDir:       "relative/path",
+			setMethod:     "env",
+			isValid:       false,
+			errorContains: "not an absolute path",
+		},
+		{
+			name:      "Env: Valid tilde path",
+			dataDir:   "~/some/path",
+			setMethod: "env",
+			isValid:   true,
+			expected:  filepath.Join(homeDir, "some/path"),
+		},
+		{
+			name:      "Env: Valid absolute path",
+			dataDir:   "/absolute/path",
+			setMethod: "env",
+			isValid:   true,
+			expected:  "/absolute/path",
+		},
+		// Config file cases
+		{
+			name:          "File: Invalid relative path",
+			dataDir:       "relative/path",
+			setMethod:     "file",
+			isValid:       false,
+			errorContains: "not an absolute path",
+		},
+		{
+			name:      "File: Valid tilde path",
+			dataDir:   "~/some/path",
+			setMethod: "file",
+			isValid:   true,
+			expected:  filepath.Join(homeDir, "some/path"),
+		},
+		{
+			name:      "File: Valid absolute path",
+			dataDir:   "/absolute/path",
+			setMethod: "file",
+			isValid:   true,
+			expected:  "/absolute/path",
+		},
+		// Default value cases
+		{
+			name:          "Default: Invalid relative path",
+			dataDir:       "relative/path",
+			setMethod:     "default",
+			isValid:       false,
+			errorContains: "not an absolute path",
+		},
+		{
+			name:      "Default: Valid tilde path",
+			dataDir:   "~/some/path",
+			setMethod: "default",
+			isValid:   true,
+			expected:  filepath.Join(homeDir, "some/path"),
+		},
+		{
+			name:      "Default: Valid absolute path",
+			dataDir:   "/absolute/path",
+			setMethod: "default",
+			isValid:   true,
+			expected:  "/absolute/path",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			var cfg *config.Config
+
+			switch tc.setMethod {
+			case "cli":
+				flags := make(map[string][]*pflag.Flag)
+				flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError)
+				flagSet.String(types.DataDirKey, tc.dataDir, "data directory")
+				flag := flagSet.Lookup(types.DataDirKey)
+				flag.Changed = true
+				flags[types.DataDirKey] = []*pflag.Flag{flag}
+				cfg, err = config.New(config.WithFlags(flags))
+			case "config":
+				cfg, err = config.New(config.WithValues(map[string]interface{}{types.DataDirKey: tc.dataDir}))
+			case "env":
+				t.Setenv("BACALHAU_DATADIR", tc.dataDir)
+				cfg, err = config.New(config.WithEnvironmentVariables(map[string][]string{
+					types.DataDirKey: {"BACALHAU_DATADIR"},
+				}))
+			case "file":
+				tempFile, innerErr := os.CreateTemp("", "config*.yaml")
+				require.NoError(t, innerErr)
+				defer os.Remove(tempFile.Name())
+				_, innerErr = tempFile.WriteString(fmt.Sprintf("datadir: %s\n", tc.dataDir))
+				require.NoError(t, innerErr)
+				tempFile.Close()
+				cfg, err = config.New(config.WithPaths(tempFile.Name()))
+			case "default":
+				cfg, err = config.New(config.WithDefault(types.Bacalhau{DataDir: tc.dataDir}))
+			}
+
+			if tc.isValid {
+				require.NoError(t, err)
+				var actual types.Bacalhau
+				assert.NoError(t, cfg.Unmarshal(&actual))
+				assert.Equal(t, tc.expected, actual.DataDir)
+			} else {
+				require.Error(t, err)
+				assert.Contains(t, err.Error(), tc.errorContains)
+			}
+		})
+	}
+}
+
 func TestConfigWithValueOverrides(t *testing.T) {
-	overrideRepo := "overrideRepo"
+	overrideRepo := "/overrideRepo"
 	overrideName := "puuid"
 	overrideClientAddress := "1.1.1.1"
 	overrideClientPort := 1234
 
 	defaultConfig := types.Bacalhau{
-		DataDir: "defaultRepo",
+		DataDir: "/defaultRepo",
 		API: types.API{
 			Host: "0.0.0.0",
 			Port: 1234,
@@ -59,10 +231,7 @@ type TestConfig struct {
 	StringValue string
 	IntValue    int
 	BoolValue   bool
-}
-
-func (c TestConfig) Validate() error {
-	return nil
+	DataDir     string
 }
 
 func TestNew(t *testing.T) {
@@ -71,6 +240,7 @@ func TestNew(t *testing.T) {
 			StringValue: "default",
 			IntValue:    42,
 			BoolValue:   true,
+			DataDir:     "/data/dir",
 		}))
 		require.NoError(t, err)
 
@@ -92,6 +262,7 @@ func TestNew(t *testing.T) {
 stringValue: "from_file"
 intValue: 100
 boolValue: false
+datadir:   "/data/dir"
 `)
 		require.NoError(t, err)
 		tempFile.Close()
@@ -134,6 +305,7 @@ boolValue: false
 				StringValue: "default",
 				IntValue:    42,
 				BoolValue:   true,
+				DataDir:     "/data/dir",
 			}),
 			config.WithFlags(flags),
 		)
@@ -149,14 +321,9 @@ boolValue: false
 	})
 
 	t.Run("With Environment Variables", func(t *testing.T) {
-		os.Setenv("BACALHAU_STRING_VALUE", "from_env")
-		os.Setenv("BACALHAU_INT_VALUE", "300")
-		os.Setenv("BACALHAU_BOOL_VALUE", "true")
-		defer func() {
-			os.Unsetenv("BACALHAU_STRING_VALUE")
-			os.Unsetenv("BACALHAU_INT_VALUE")
-			os.Unsetenv("BACALHAU_BOOL_VALUE")
-		}()
+		t.Setenv("BACALHAU_STRING_VALUE", "from_env")
+		t.Setenv("BACALHAU_INT_VALUE", "300")
+		t.Setenv("BACALHAU_BOOL_VALUE", "true")
 
 		envVars := map[string][]string{
 			"stringValue": {"BACALHAU_STRING_VALUE"},
@@ -169,6 +336,7 @@ boolValue: false
 				StringValue: "default",
 				IntValue:    42,
 				BoolValue:   false,
+				DataDir:     "/data/dir",
 			}),
 			config.WithEnvironmentVariables(envVars),
 		)
@@ -188,6 +356,7 @@ boolValue: false
 			"stringValue": "from_values",
 			"intValue":    400,
 			"boolValue":   false,
+			"datadir":     "/data/dir",
 		}
 
 		cfg, err := config.New(
@@ -228,6 +397,7 @@ boolValue: true
 			StringValue: "default",
 			IntValue:    42,
 			BoolValue:   false,
+			DataDir:     "/data/dir",
 		}),
 	)
 	require.NoError(t, err)
@@ -252,6 +422,7 @@ func TestMerge(t *testing.T) {
 	_, err = tempFile1.WriteString(`
 stringValue: "first"
 intValue: 100
+datadir: "/dir1"
 `)
 	require.NoError(t, err)
 	tempFile1.Close()
@@ -263,6 +434,7 @@ intValue: 100
 	_, err = tempFile2.WriteString(`
 intValue: 200
 boolValue: true
+datadir: "/dir2"
 `)
 	require.NoError(t, err)
 	tempFile2.Close()
@@ -273,13 +445,10 @@ boolValue: true
 			IntValue:    42,
 			BoolValue:   false,
 		}),
-		config.WithPaths(tempFile1.Name()),
+		config.WithPaths(tempFile1.Name(), tempFile2.Name()),
 	)
 	require.NoError(t, err)
 
-	err = cfg.Merge(tempFile2.Name())
-	require.NoError(t, err)
-
 	var testCfg TestConfig
 	err = cfg.Unmarshal(&testCfg)
 	require.NoError(t, err)
@@ -287,6 +456,7 @@ boolValue: true
 	assert.Equal(t, "first", testCfg.StringValue)
 	assert.Equal(t, 200, testCfg.IntValue)
 	assert.True(t, testCfg.BoolValue)
+	assert.Equal(t, "/dir2", testCfg.DataDir)
 }
 
 func TestConfigurationPrecedence(t *testing.T) {
@@ -304,14 +474,9 @@ boolValue: false
 	tempFile.Close()
 
 	// Set up environment variables
-	os.Setenv("BACALHAU_STRING_VALUE", "from_env")
-	os.Setenv("BACALHAU_INT_VALUE", "200")
-	os.Setenv("BACALHAU_BOOL_VALUE", "true")
-	defer func() {
-		os.Unsetenv("BACALHAU_STRING_VALUE")
-		os.Unsetenv("BACALHAU_INT_VALUE")
-		os.Unsetenv("BACALHAU_BOOL_VALUE")
-	}()
+	t.Setenv("BACALHAU_STRING_VALUE", "from_env")
+	t.Setenv("BACALHAU_INT_VALUE", "200")
+	t.Setenv("BACALHAU_BOOL_VALUE", "true")
 
 	// Set up explicit values
 	values := map[string]interface{}{
@@ -326,6 +491,7 @@ boolValue: false
 			StringValue: "default",
 			IntValue:    50,
 			BoolValue:   false,
+			DataDir:     "/data/dir",
 		}),
 		config.WithPaths(tempFile.Name()),
 		config.WithEnvironmentVariables(map[string][]string{
@@ -367,6 +533,7 @@ boolValue: false
 			StringValue: "default",
 			IntValue:    50,
 			BoolValue:   false,
+			DataDir:     "/data/dir",
 		}),
 		config.WithPaths(tempFile.Name()),
 		config.WithEnvironmentVariables(map[string][]string{
@@ -391,6 +558,7 @@ boolValue: false
 			StringValue: "default",
 			IntValue:    50,
 			BoolValue:   false,
+			DataDir:     "/data/dir",
 		}),
 		config.WithPaths(tempFile.Name()),
 		config.WithEnvironmentVariables(map[string][]string{
@@ -418,6 +586,7 @@ boolValue: false
 			StringValue: "default",
 			IntValue:    50,
 			BoolValue:   true,
+			DataDir:     "/data/dir",
 		}),
 		config.WithPaths(tempFile.Name()),
 	)
@@ -436,6 +605,7 @@ boolValue: false
 			StringValue: "default",
 			IntValue:    50,
 			BoolValue:   true,
+			DataDir:     "/data/dir",
 		}),
 	)
 	require.NoError(t, err)
@@ -461,6 +631,7 @@ func TestFlagConfigConflicts(t *testing.T) {
 
 		values := map[string]any{
 			"boolValue": true,
+			"datadir":   "/data/dir",
 		}
 
 		cfg, err := config.New(