From c148dd69ca8eeedc222ba36e328e7c5aacc5ef7c Mon Sep 17 00:00:00 2001 From: xgopilot Date: Sat, 2 May 2026 04:57:31 +0000 Subject: [PATCH] test(rules): improve coverage for rule loading Generated with [codeagent](https://github.com/qbox/codeagent) Co-authored-by: phantom5099 <245659304+phantom5099@users.noreply.github.com> --- internal/context/sources_test.go | 12 +++-- internal/rules/loader_test.go | 47 ++++++++++++++++++ internal/rules/store.go | 7 +++ internal/rules/store_test.go | 81 ++++++++++++++++++++++++++++++++ 4 files changed, 144 insertions(+), 3 deletions(-) diff --git a/internal/context/sources_test.go b/internal/context/sources_test.go index ba3072de..31e11a9d 100644 --- a/internal/context/sources_test.go +++ b/internal/context/sources_test.go @@ -9,6 +9,7 @@ import ( "testing" "neo-code/internal/promptasset" + "neo-code/internal/rules" ) func TestCorePromptSourceSectionsReturnsClone(t *testing.T) { @@ -37,8 +38,9 @@ func TestCorePromptSourceSectionsReturnsClone(t *testing.T) { func TestRulesPromptSourceSectionsSkipsWhenNoRulesExist(t *testing.T) { t.Parallel() - sections, err := newRulesPromptSource(nil).Sections(context.Background(), BuildInput{ - Metadata: Metadata{Workdir: t.TempDir()}, + baseDir := filepath.Join(t.TempDir(), ".neocode") + sections, err := newRulesPromptSource(rules.NewLoader(baseDir)).Sections(context.Background(), BuildInput{ + Metadata: Metadata{ProjectRoot: t.TempDir(), Workdir: t.TempDir()}, }) if err != nil { t.Fatalf("Sections() error = %v", err) @@ -50,11 +52,15 @@ func TestRulesPromptSourceSectionsSkipsWhenNoRulesExist(t *testing.T) { func TestRulesPromptSourceSectionsRendersRules(t *testing.T) { root := t.TempDir() + baseDir := filepath.Join(t.TempDir(), ".neocode") + if err := os.MkdirAll(baseDir, 0o755); err != nil { + t.Fatalf("mkdir baseDir: %v", err) + } if err := os.WriteFile(filepath.Join(root, projectRuleFileName), []byte("rule-body"), 0o644); err != nil { t.Fatalf("write AGENTS.md: %v", err) } - sections, err := newRulesPromptSource(nil).Sections(context.Background(), BuildInput{ + sections, err := newRulesPromptSource(rules.NewLoader(baseDir)).Sections(context.Background(), BuildInput{ Metadata: Metadata{ProjectRoot: root, Workdir: root}, }) if err != nil { diff --git a/internal/rules/loader_test.go b/internal/rules/loader_test.go index 30a1b089..de8059ae 100644 --- a/internal/rules/loader_test.go +++ b/internal/rules/loader_test.go @@ -115,3 +115,50 @@ func TestLoaderLoadKeepsGlobalRulesWhenProjectRulesMissing(t *testing.T) { t.Fatalf("expected global-only rules, got %+v", snapshot.GlobalAGENTS) } } + +func TestLoaderLoadHonorsCanceledContext(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + _, err := NewLoader(filepath.Join(t.TempDir(), ".neocode")).Load(ctx, t.TempDir()) + if err == nil || !strings.Contains(err.Error(), context.Canceled.Error()) { + t.Fatalf("expected canceled error, got %v", err) + } +} + +func TestLoaderLoadUsesHomeFallbackWhenBaseDirEmpty(t *testing.T) { + homeDir := t.TempDir() + t.Setenv("HOME", homeDir) + baseDir := filepath.Join(homeDir, defaultRulesDir) + if err := os.MkdirAll(baseDir, 0o755); err != nil { + t.Fatalf("mkdir baseDir: %v", err) + } + if err := os.WriteFile(filepath.Join(baseDir, agentsFileName), []byte("global-home"), 0o644); err != nil { + t.Fatalf("write global AGENTS.md: %v", err) + } + + snapshot, err := NewLoader("").Load(context.Background(), "") + if err != nil { + t.Fatalf("Load() error = %v", err) + } + if snapshot.GlobalAGENTS.Content != "global-home" { + t.Fatalf("expected home fallback rules, got %+v", snapshot.GlobalAGENTS) + } +} + +func TestResolveBaseDirCleansExplicitBaseDir(t *testing.T) { + got := resolveBaseDir(filepath.Join(t.TempDir(), "nested", "..", ".neocode")) + if !filepath.IsAbs(got) { + t.Fatalf("expected absolute cleaned baseDir, got %q", got) + } + if filepath.Base(got) != defaultRulesDir { + t.Fatalf("expected %q suffix, got %q", defaultRulesDir, got) + } +} + +func TestTruncateRunesWithZeroBudget(t *testing.T) { + got, truncated := truncateRunes("规则", 0) + if got != "" || !truncated { + t.Fatalf("truncateRunes() = (%q, %v), want empty truncated result", got, truncated) + } +} diff --git a/internal/rules/store.go b/internal/rules/store.go index 4eb86cfa..9ec007c2 100644 --- a/internal/rules/store.go +++ b/internal/rules/store.go @@ -26,6 +26,13 @@ func ProjectRulePath(projectRoot string) string { if root == "" { return "" } + if !filepath.IsAbs(root) { + absRoot, err := filepath.Abs(root) + if err != nil { + return "" + } + root = absRoot + } info, err := os.Stat(root) if err == nil && !info.IsDir() { diff --git a/internal/rules/store_test.go b/internal/rules/store_test.go index 38e0dfcf..c625a306 100644 --- a/internal/rules/store_test.go +++ b/internal/rules/store_test.go @@ -43,6 +43,24 @@ func TestProjectRulePathUsesFileParentDirectory(t *testing.T) { } } +func TestProjectRulePathNormalizesRelativeRootToAbsolute(t *testing.T) { + tempRoot := t.TempDir() + workdir, err := os.Getwd() + if err != nil { + t.Fatalf("os.Getwd() error = %v", err) + } + relativeRoot, err := filepath.Rel(workdir, tempRoot) + if err != nil { + t.Fatalf("filepath.Rel() error = %v", err) + } + + got := ProjectRulePath(relativeRoot) + want := filepath.Join(tempRoot, agentsFileName) + if got != want { + t.Fatalf("ProjectRulePath(relative) = %q, want %q", got, want) + } +} + func TestWriteGlobalRuleCreatesFileAndCanBeReadBack(t *testing.T) { baseDir := filepath.Join(t.TempDir(), ".neocode") path, err := WriteGlobalRule(context.Background(), baseDir, "默认使用中文输出") @@ -85,6 +103,46 @@ func TestWriteProjectRuleCreatesFileAndCanBeReadBack(t *testing.T) { } } +func TestReadGlobalRuleHonorsCanceledContext(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + _, err := ReadGlobalRule(ctx, filepath.Join(t.TempDir(), ".neocode")) + if err == nil || !strings.Contains(err.Error(), context.Canceled.Error()) { + t.Fatalf("expected canceled error, got %v", err) + } +} + +func TestReadProjectRuleHonorsCanceledContext(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + _, err := ReadProjectRule(ctx, t.TempDir()) + if err == nil || !strings.Contains(err.Error(), context.Canceled.Error()) { + t.Fatalf("expected canceled error, got %v", err) + } +} + +func TestWriteGlobalRuleHonorsCanceledContext(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + _, err := WriteGlobalRule(ctx, filepath.Join(t.TempDir(), ".neocode"), "ignored") + if err == nil || !strings.Contains(err.Error(), context.Canceled.Error()) { + t.Fatalf("expected canceled error, got %v", err) + } +} + +func TestWriteProjectRuleHonorsCanceledContext(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + _, err := WriteProjectRule(ctx, t.TempDir(), "ignored") + if err == nil || !strings.Contains(err.Error(), context.Canceled.Error()) { + t.Fatalf("expected canceled error, got %v", err) + } +} + func TestWriteGlobalRuleRejectsInvalidUTF8(t *testing.T) { baseDir := filepath.Join(t.TempDir(), ".neocode") _, err := WriteGlobalRule(context.Background(), baseDir, string([]byte{0xff, 0xfe})) @@ -103,3 +161,26 @@ func TestReadGlobalRuleReturnsEmptyWhenMissing(t *testing.T) { t.Fatalf("expected empty document, got %+v", document) } } + +func TestReadProjectRuleReturnsEmptyWhenMissing(t *testing.T) { + document, err := ReadProjectRule(context.Background(), t.TempDir()) + if err != nil { + t.Fatalf("ReadProjectRule() error = %v", err) + } + if document != (Document{}) { + t.Fatalf("expected empty document, got %+v", document) + } +} + +func TestReadProjectRuleReturnsReadError(t *testing.T) { + projectRoot := t.TempDir() + rulePath := filepath.Join(projectRoot, agentsFileName) + if err := os.MkdirAll(rulePath, 0o755); err != nil { + t.Fatalf("mkdir rule path: %v", err) + } + + _, err := ReadProjectRule(context.Background(), projectRoot) + if err == nil || !strings.Contains(err.Error(), "rules: read") { + t.Fatalf("expected read error, got %v", err) + } +}