test(config): tests for the data directory paths

Christian Rocha and Charm Crush created

Co-Authored-By: Charm Crush <crush@charm.land>

Change summary

internal/config/config.go     | 18 +++++++++++-------
internal/config/load_test.go  | 30 ++++++++++++++++++++++++++++++
internal/swagger/docs.go      |  2 +-
internal/swagger/swagger.json |  2 +-
internal/swagger/swagger.yaml |  6 +++++-
schema.json                   |  2 +-
6 files changed, 49 insertions(+), 11 deletions(-)

Detailed changes

internal/config/config.go 🔗

@@ -259,13 +259,17 @@ func (Attribution) JSONSchemaExtend(schema *jsonschema.Schema) {
 }
 
 type Options struct {
-	ContextPaths              []string     `json:"context_paths,omitempty" jsonschema:"description=Paths to files containing context information for the AI,example=.cursorrules,example=CRUSH.md"`
-	SkillsPaths               []string     `json:"skills_paths,omitempty" jsonschema:"description=Paths to directories containing Agent Skills (folders with SKILL.md files),example=~/.config/crush/skills,example=./skills"`
-	TUI                       *TUIOptions  `json:"tui,omitempty" jsonschema:"description=Terminal user interface options"`
-	Debug                     bool         `json:"debug,omitempty" jsonschema:"description=Enable debug logging,default=false"`
-	DebugLSP                  bool         `json:"debug_lsp,omitempty" jsonschema:"description=Enable debug logging for LSP servers,default=false"`
-	DisableAutoSummarize      bool         `json:"disable_auto_summarize,omitempty" jsonschema:"description=Disable automatic conversation summarization,default=false"`
-	DataDirectory             string       `json:"data_directory,omitempty" jsonschema:"description=Directory for storing application data (relative to working directory),default=.crush,example=.crush"` // Relative to the cwd
+	ContextPaths         []string    `json:"context_paths,omitempty" jsonschema:"description=Paths to files containing context information for the AI,example=.cursorrules,example=CRUSH.md"`
+	SkillsPaths          []string    `json:"skills_paths,omitempty" jsonschema:"description=Paths to directories containing Agent Skills (folders with SKILL.md files),example=~/.config/crush/skills,example=./skills"`
+	TUI                  *TUIOptions `json:"tui,omitempty" jsonschema:"description=Terminal user interface options"`
+	Debug                bool        `json:"debug,omitempty" jsonschema:"description=Enable debug logging,default=false"`
+	DebugLSP             bool        `json:"debug_lsp,omitempty" jsonschema:"description=Enable debug logging for LSP servers,default=false"`
+	DisableAutoSummarize bool        `json:"disable_auto_summarize,omitempty" jsonschema:"description=Disable automatic conversation summarization,default=false"`
+	// DataDirectory is where Crush keeps per-project state such as
+	// the SQLite database and workspace overrides. Relative paths are
+	// resolved against the working directory; absolute paths are used
+	// verbatim. After defaulting the stored value is always absolute.
+	DataDirectory             string       `json:"data_directory,omitempty" jsonschema:"description=Directory for storing application data. Relative paths are resolved against the working directory; absolute paths are used as-is.,default=.crush,example=.crush"`
 	DisabledTools             []string     `json:"disabled_tools,omitempty" jsonschema:"description=List of built-in tools to disable and hide from the agent,example=bash,example=sourcegraph"`
 	DisableProviderAutoUpdate bool         `json:"disable_provider_auto_update,omitempty" jsonschema:"description=Disable providers auto-update,default=false"`
 	DisableDefaultProviders   bool         `json:"disable_default_providers,omitempty" jsonschema:"description=Ignore all default/embedded providers. When enabled\\, providers must be fully specified in the config file with base_url\\, models\\, and api_key - no merging with defaults occurs,default=false"`

internal/config/load_test.go 🔗

@@ -205,6 +205,36 @@ func TestConfig_setDefaults(t *testing.T) {
 		require.Equal(t, filepath.Join(workingDir, "state"), cfg.Options.DataDirectory)
 	})
 
+	t.Run("preserves absolute configured data directory", func(t *testing.T) {
+		// Use a platform-appropriate absolute path so the test runs
+		// the same way on POSIX and Windows.
+		absDir := filepath.Join(t.TempDir(), "data")
+		cfg := &Config{Options: &Options{DataDirectory: absDir}}
+
+		cfg.setDefaults(filepath.Join(t.TempDir(), "worktree"), "")
+
+		require.Equal(t, absDir, cfg.Options.DataDirectory)
+	})
+
+	t.Run("workspace merge re-entry keeps an absolute data directory", func(t *testing.T) {
+		// Simulate the load and reload paths: defaults are applied
+		// twice with the data directory potentially carried through
+		// from an earlier merge as a relative string.
+		workingDir := filepath.Join(t.TempDir(), "worktree")
+		cfg := &Config{}
+		cfg.setDefaults(workingDir, "")
+
+		// Workspace JSON sets data_directory to a relative value; the
+		// merge replaces the struct, then setDefaults runs again.
+		cfg.Options.DataDirectory = "./state"
+		cfg.setDefaults(workingDir, "")
+
+		require.True(t, filepath.IsAbs(cfg.Options.DataDirectory),
+			"data directory must remain absolute after re-merge, got %q",
+			cfg.Options.DataDirectory)
+		require.Equal(t, filepath.Join(workingDir, "state"), cfg.Options.DataDirectory)
+	})
+
 	t.Run("does not adopt .crush from a parent project", func(t *testing.T) {
 		parent := t.TempDir()
 

internal/swagger/docs.go 🔗

@@ -2939,7 +2939,7 @@ const docTemplate = `{
                     }
                 },
                 "data_directory": {
-                    "description": "Relative to the cwd",
+                    "description": "DataDirectory is where Crush keeps per-project state such as the SQLite database and workspace overrides. Relative paths are resolved against the working directory; absolute paths are used verbatim. After defaulting the stored value is always absolute.",
                     "type": "string"
                 },
                 "debug": {

internal/swagger/swagger.json 🔗

@@ -2932,7 +2932,7 @@
                     }
                 },
                 "data_directory": {
-                    "description": "Relative to the cwd",
+                    "description": "DataDirectory is where Crush keeps per-project state such as the SQLite database and workspace overrides. Relative paths are resolved against the working directory; absolute paths are used verbatim. After defaulting the stored value is always absolute.",
                     "type": "string"
                 },
                 "debug": {

internal/swagger/swagger.yaml 🔗

@@ -287,7 +287,11 @@ definitions:
           type: string
         type: array
       data_directory:
-        description: Relative to the cwd
+        description: |-
+          DataDirectory is where Crush keeps per-project state such as the SQLite
+          database and workspace overrides. Relative paths are resolved against
+          the working directory; absolute paths are used verbatim. After
+          defaulting the stored value is always absolute.
         type: string
       debug:
         type: boolean

schema.json 🔗

@@ -423,7 +423,7 @@
         },
         "data_directory": {
           "type": "string",
-          "description": "Directory for storing application data (relative to working directory)",
+          "description": "Directory for storing application data. Relative paths are resolved against the working directory; absolute paths are used as-is.",
           "default": ".crush",
           "examples": [
             ".crush"