Skip to content

Commit e75f978

Browse files
tae2089claude
andcommitted
fix: honor config-file namespace over flag default in CLI commands
Add resolveNamespace() so a namespace set only in .ccg.yaml is not masked by the --namespace flag's default value. Priority: explicit --namespace flag > config namespace > default. Applied across build, docs, lint, search, status, and update commands, with tests for search and status. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent ae09de9 commit e75f978

10 files changed

Lines changed: 105 additions & 8 deletions

File tree

.ccg.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ db:
33
driver: postgres
44
dsn: "host=localhost user=postgres password=postgres dbname=ccg_test sslmode=disable"
55

6-
namespace: default
6+
namespace: ccg
77

88
output:
99
dir: ./docs

internal/adapters/inbound/cli/build.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ func newBuildCmd(deps *Deps) *cobra.Command {
5656
}
5757

5858
ctx := context.Background()
59-
ns, _ := cmd.Flags().GetString("namespace")
59+
ns := resolveNamespace(cmd)
6060
ctx = requestctx.WithNamespace(ctx, ns)
6161
stats, err := svc.Build(ctx, opts)
6262
if err != nil {

internal/adapters/inbound/cli/docs.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ func newDocsCmd(deps *Deps) *cobra.Command {
4545
Files: contentfiles.NewRoot(absOut),
4646
OutDir: absOut,
4747
Exclude: resolveExcludes(excludePatterns),
48-
Namespace: viper.GetString("namespace"),
48+
Namespace: resolveNamespace(cmd),
4949
Prune: prune,
5050
}
5151

@@ -58,7 +58,7 @@ func newDocsCmd(deps *Deps) *cobra.Command {
5858
OutDir: absOut,
5959
IndexDir: resolveRagIndexDir(ragIndexDir),
6060
ProjectDesc: resolveRagDescription(projectDesc),
61-
Namespace: viper.GetString("namespace"),
61+
Namespace: resolveNamespace(cmd),
6262
Exclude: resolveExcludes(excludePatterns),
6363
})
6464
if err != nil {

internal/adapters/inbound/cli/lint.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -443,7 +443,7 @@ func newLintCmd(deps *Deps) *cobra.Command {
443443
Files: contentfiles.NewRoot(absOut),
444444
OutDir: absOut,
445445
Exclude: resolveExcludes(excludePatterns),
446-
Namespace: viper.GetString("namespace"),
446+
Namespace: resolveNamespace(cmd),
447447
}
448448

449449
report, err := gen.Lint()

internal/adapters/inbound/cli/root.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,21 @@ func resolveOutDir(flagValue string) string {
200200
return flagValue
201201
}
202202

203+
// resolveNamespace returns the effective namespace for a command, reading through
204+
// viper instead of cmd.Flags() so a namespace set only in the config file is not
205+
// masked by the --namespace flag's default value.
206+
// @intent config의 namespace 설정이 --namespace 플래그 기본값에 가려지지 않도록 우선순위대로 해석한다.
207+
// @ensures 우선순위는 명시적 --namespace 플래그 > CCG_NAMESPACE 환경변수 > config namespace > 기본값(default) 순이다.
208+
func resolveNamespace(cmd *cobra.Command) string {
209+
if flag := cmd.Flags().Lookup("namespace"); flag != nil && flag.Changed {
210+
return flag.Value.String()
211+
}
212+
if ns := viper.GetString("namespace"); ns != "" {
213+
return ns
214+
}
215+
return requestctx.DefaultNamespace
216+
}
217+
203218
// resolveExcludes merges exclude patterns from the config file (viper "exclude"
204219
// key) and the command-line flag, deduplicating nothing — order is config first,
205220
// then flags.

internal/adapters/inbound/cli/search.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ func newSearchCmd(deps *Deps) *cobra.Command {
2929
return fmt.Errorf("limit must be > 0, got %d", limit)
3030
}
3131
ctx := cmd.Context()
32-
ns, _ := cmd.Flags().GetString("namespace")
32+
ns := resolveNamespace(cmd)
3333
ctx = requestctx.WithNamespace(ctx, ns)
3434

3535
// Over-fetch a wider candidate pool so structural reranking can

internal/adapters/inbound/cli/search_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"strings"
88
"testing"
99

10+
"github.com/spf13/viper"
1011
"gorm.io/driver/sqlite"
1112
"gorm.io/gorm"
1213
gormlogger "gorm.io/gorm/logger"
@@ -280,6 +281,52 @@ func TestSearchCommand_RejectsNonPositiveLimit(t *testing.T) {
280281
}
281282
}
282283

284+
func TestSearchCommand_NamespaceFromConfig(t *testing.T) {
285+
viper.Reset()
286+
defer viper.Reset()
287+
288+
deps, stdout, stderr, _ := setupSearchTest(t)
289+
290+
var gotNS string
291+
deps.SearchReader = &spySearchBackend{queryFn: func(ctx context.Context, query string, queryLimit int) ([]graph.Node, error) {
292+
gotNS = requestctx.FromContext(ctx)
293+
return nil, nil
294+
}}
295+
296+
// Config selects namespace=backend; no --namespace flag is passed.
297+
viper.Set("namespace", "backend")
298+
299+
if err := executeCmd(deps, stdout, stderr, "search", "hello"); err != nil {
300+
t.Fatalf("search: %v", err)
301+
}
302+
if gotNS != "backend" {
303+
t.Fatalf("expected config namespace 'backend', got %q", gotNS)
304+
}
305+
}
306+
307+
func TestSearchCommand_FlagOverridesConfigNamespace(t *testing.T) {
308+
viper.Reset()
309+
defer viper.Reset()
310+
311+
deps, stdout, stderr, _ := setupSearchTest(t)
312+
313+
var gotNS string
314+
deps.SearchReader = &spySearchBackend{queryFn: func(ctx context.Context, query string, queryLimit int) ([]graph.Node, error) {
315+
gotNS = requestctx.FromContext(ctx)
316+
return nil, nil
317+
}}
318+
319+
// Explicit --namespace must win over the config value.
320+
viper.Set("namespace", "backend")
321+
322+
if err := executeCmd(deps, stdout, stderr, "--namespace", "frontend", "search", "hello"); err != nil {
323+
t.Fatalf("search: %v", err)
324+
}
325+
if gotNS != "frontend" {
326+
t.Fatalf("expected flag namespace 'frontend' to override config, got %q", gotNS)
327+
}
328+
}
329+
283330
func TestSearchCommand_UsesCommandContext(t *testing.T) {
284331
deps, stdout, stderr, _ := setupSearchTest(t)
285332
ctx, cancel := context.WithCancel(context.Background())

internal/adapters/inbound/cli/status.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ func newStatusCmd(deps *Deps) *cobra.Command {
2727
return errDBNotInitialized
2828
}
2929

30-
ns, _ := cmd.Flags().GetString("namespace")
30+
ns := resolveNamespace(cmd)
3131
ctx := requestctx.WithNamespace(cmd.Context(), ns)
3232
stats, err := deps.Statistics.GraphStatistics(ctx)
3333
if err != nil {

internal/adapters/inbound/cli/status_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"strings"
66
"testing"
77

8+
"github.com/spf13/viper"
89
"gorm.io/driver/sqlite"
910
"gorm.io/gorm"
1011
gormlogger "gorm.io/gorm/logger"
@@ -322,3 +323,37 @@ func TestStatusCommand_RespectsNamespace(t *testing.T) {
322323
t.Fatalf("unexpected cross-namespace aggregation: %s", out)
323324
}
324325
}
326+
327+
func TestStatusCommand_NamespaceFromConfig(t *testing.T) {
328+
viper.Reset()
329+
defer viper.Reset()
330+
331+
deps, stdout, stderr, db := setupStatusTest(t)
332+
333+
// One node in the default namespace, two in "backend".
334+
if err := db.Create(&graph.Node{Namespace: requestctx.DefaultNamespace, QualifiedName: "default.Foo", Kind: graph.NodeKindFunction, Name: "Foo", FilePath: "default/foo.go", StartLine: 1, EndLine: 2, Language: "go"}).Error; err != nil {
335+
t.Fatal(err)
336+
}
337+
if err := db.Create(&graph.Node{Namespace: "backend", QualifiedName: "backend.Bar", Kind: graph.NodeKindFunction, Name: "Bar", FilePath: "backend/bar.go", StartLine: 1, EndLine: 2, Language: "go"}).Error; err != nil {
338+
t.Fatal(err)
339+
}
340+
if err := db.Create(&graph.Node{Namespace: "backend", QualifiedName: "backend.Baz", Kind: graph.NodeKindFunction, Name: "Baz", FilePath: "backend/baz.go", StartLine: 1, EndLine: 2, Language: "go"}).Error; err != nil {
341+
t.Fatal(err)
342+
}
343+
344+
// Config selects namespace=backend; no --namespace flag is passed, so the
345+
// config value must win over the flag's default.
346+
viper.Set("namespace", "backend")
347+
348+
stdout.Reset()
349+
stderr.Reset()
350+
351+
if err := executeCmd(deps, stdout, stderr, "status"); err != nil {
352+
t.Fatalf("status: %v", err)
353+
}
354+
355+
out := stdout.String()
356+
if !strings.Contains(out, "Nodes: 2") {
357+
t.Fatalf("expected config namespace 'backend' (2 nodes), got: %s", out)
358+
}
359+
}

internal/adapters/inbound/cli/update.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ func newUpdateCmd(deps *Deps) *cobra.Command {
3737
fileLimit := resolveMaxFileBytes(maxFileBytes)
3838
totalLimit := resolveMaxTotalParsedBytes(maxTotalParsedBytes)
3939
ctx := cmd.Context()
40-
ns, _ := cmd.Flags().GetString("namespace")
40+
ns := resolveNamespace(cmd)
4141
ctx = requestctx.WithNamespace(ctx, ns)
4242

4343
svc := &workflow.Service{

0 commit comments

Comments
 (0)