on main help sign in
fix(config): scope crush.json discovery to the current repo
e112368Christian Rocha
internal/config/load.go+7−2
@@ -702,7 +702,12 @@ func configureSelectedModels(store *ConfigStore, knownProviders []catwalk.Provid
702 return nil
702 return nil
703}
703}
704
704
705−// lookupConfigs searches config files recursively from CWD up to FS root
705+// lookupConfigs searches config files starting at cwd and walking up
706+// through the current project. The upward walk stops at the git
707+// working tree root when one can be detected, otherwise at cwd itself,
708+// so an unrelated crush.json placed above the project is never picked
709+// up. Global user-level config locations are always included
710+// regardless of the boundary.
706func lookupConfigs(cwd string) []string {
711func lookupConfigs(cwd string) []string {
707 // prepend default config paths
712 // prepend default config paths
708 configPaths := []string{
713 configPaths := []string{
@@ -712,7 +717,7 @@ func lookupConfigs(cwd string) []string {
712
717
713 configNames := []string{appName + ".json", "." + appName + ".json"}
718 configNames := []string{appName + ".json", "." + appName + ".json"}
714
719
715− foundConfigs, err := fsext.Lookup(cwd, configNames...)
720+ foundConfigs, err := fsext.LookupBounded(cwd, projectBoundary(cwd), configNames...)
716 if err != nil {
721 if err != nil {
717 // returns at least default configs
722 // returns at least default configs
718 return configPaths
723 return configPaths
internal/config/load_test.go+92
@@ -37,6 +37,98 @@ func TestConfig_LoadFromBytes(t *testing.T) {
37 require.Equal(t, "https://api.openai.com/v2", pc.BaseURL)
37 require.Equal(t, "https://api.openai.com/v2", pc.BaseURL)
38}
38}
39
39
40+func TestLookupConfigs_BoundedByProject(t *testing.T) {
41+ // Force GlobalConfig and GlobalConfigData to point at locations we
42+ // control so they can be present in the result without polluting
43+ // the developer's real config.
44+ globalDir := t.TempDir()
45+ t.Setenv("CRUSH_GLOBAL_CONFIG", globalDir)
46+ t.Setenv("CRUSH_GLOBAL_DATA", globalDir)
47+
48+ t.Run("does not pick up crush.json above non-git project", func(t *testing.T) {
49+ parent := t.TempDir()
50+
51+ // crush.json above the project must not be adopted.
52+ require.NoError(t, os.WriteFile(
53+ filepath.Join(parent, "crush.json"),
54+ []byte(`{}`),
55+ 0o644,
56+ ))
57+
58+ project := filepath.Join(parent, "project")
59+ require.NoError(t, os.Mkdir(project, 0o755))
60+
61+ got := lookupConfigs(project)
62+ for _, p := range got {
63+ require.NotEqual(t, filepath.Join(parent, "crush.json"), p)
64+ }
65+ })
66+
67+ t.Run("does not climb out of git worktree to find crush.json", func(t *testing.T) {
68+ if _, err := exec.LookPath("git"); err != nil {
69+ t.Skip("git not available")
70+ }
71+
72+ parent := t.TempDir()
73+
74+ require.NoError(t, os.WriteFile(
75+ filepath.Join(parent, "crush.json"),
76+ []byte(`{}`),
77+ 0o644,
78+ ))
79+
80+ worktree := filepath.Join(parent, "worktree")
81+ require.NoError(t, os.Mkdir(worktree, 0o755))
82+ gitInit := exec.CommandContext(t.Context(), "git", "init", "-q")
83+ gitInit.Dir = worktree
84+ require.NoError(t, gitInit.Run())
85+
86+ got := lookupConfigs(worktree)
87+ strayEval, err := filepath.EvalSymlinks(filepath.Join(parent, "crush.json"))
88+ require.NoError(t, err)
89+ for _, p := range got {
90+ pEval, err := filepath.EvalSymlinks(p)
91+ if err != nil {
92+ continue
93+ }
94+ require.NotEqual(t, strayEval, pEval, "must not adopt parent crush.json")
95+ }
96+ })
97+
98+ t.Run("picks up crush.json inside the project", func(t *testing.T) {
99+ project := t.TempDir()
100+ local := filepath.Join(project, "crush.json")
101+ require.NoError(t, os.WriteFile(local, []byte(`{}`), 0o644))
102+
103+ got := lookupConfigs(project)
104+
105+ localEval, err := filepath.EvalSymlinks(local)
106+ require.NoError(t, err)
107+ var foundLocal bool
108+ for _, p := range got {
109+ pEval, err := filepath.EvalSymlinks(p)
110+ if err != nil {
111+ continue
112+ }
113+ if pEval == localEval {
114+ foundLocal = true
115+ break
116+ }
117+ }
118+ require.True(t, foundLocal, "expected project crush.json to be in lookup result: %v", got)
119+ })
120+
121+ t.Run("global config is always included regardless of boundary", func(t *testing.T) {
122+ project := t.TempDir()
123+
124+ got := lookupConfigs(project)
125+ // Global config and global data path are always prepended,
126+ // even when no project file exists.
127+ require.Contains(t, got, GlobalConfig())
128+ require.Contains(t, got, GlobalConfigData())
129+ })
130+}
131+
40func TestLoadFromConfigPaths_InvalidJSON(t *testing.T) {
132func TestLoadFromConfigPaths_InvalidJSON(t *testing.T) {
41 t.Parallel()
133 t.Parallel()
42
134