Skip to content

Commit 297f68a

Browse files
committed
fix: always add frontmatter for cursor format
1 parent 5e5a207 commit 297f68a

2 files changed

Lines changed: 167 additions & 2 deletions

File tree

internal/formats/render_test.go

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,145 @@ func TestRenderRulesToFormat_OnlyMarkdownFiles(t *testing.T) {
599599
}
600600
}
601601

602+
// TestRenderRulesToFormat_CursorFallbackFrontmatter tests Cursor format fallback when frontmatter has only irrelevant fields
603+
func TestRenderRulesToFormat_CursorFallbackFrontmatter(t *testing.T) {
604+
tests := []struct {
605+
name string
606+
frontmatter string
607+
shouldApplyFallback bool
608+
}{
609+
{
610+
name: "Empty frontmatter should trigger fallback",
611+
frontmatter: "",
612+
shouldApplyFallback: true,
613+
},
614+
{
615+
name: "Only name property should trigger fallback",
616+
frontmatter: `---
617+
name: abc
618+
---`,
619+
shouldApplyFallback: true,
620+
},
621+
{
622+
name: "Only tags property should trigger fallback",
623+
frontmatter: `---
624+
tags: [frontend, react]
625+
---`,
626+
shouldApplyFallback: true,
627+
},
628+
{
629+
name: "Has description should not trigger fallback",
630+
frontmatter: `---
631+
description: Test description
632+
---`,
633+
shouldApplyFallback: false,
634+
},
635+
{
636+
name: "Has alwaysApply should not trigger fallback",
637+
frontmatter: `---
638+
alwaysApply: true
639+
---`,
640+
shouldApplyFallback: false,
641+
},
642+
{
643+
name: "Has globs should not trigger fallback",
644+
frontmatter: `---
645+
globs: "**/*.tsx"
646+
---`,
647+
shouldApplyFallback: false,
648+
},
649+
{
650+
name: "Has description and name should not trigger fallback",
651+
frontmatter: `---
652+
name: abc
653+
description: Test description
654+
---`,
655+
shouldApplyFallback: false,
656+
},
657+
}
658+
659+
for _, tt := range tests {
660+
t.Run(tt.name, func(t *testing.T) {
661+
// Create temporary source directory
662+
sourceDir := createTempSourceDir(t)
663+
defer os.RemoveAll(sourceDir)
664+
665+
// Create test rule content
666+
var testRuleContent string
667+
if tt.frontmatter == "" {
668+
testRuleContent = `# Test Rule
669+
670+
This is a test rule.
671+
`
672+
} else {
673+
testRuleContent = tt.frontmatter + `
674+
675+
# Test Rule
676+
677+
This is a test rule.
678+
`
679+
}
680+
681+
testRuleFile := filepath.Join(sourceDir, "test-rule.md")
682+
if err := os.WriteFile(testRuleFile, []byte(testRuleContent), 0644); err != nil {
683+
t.Fatalf("Failed to create test rule file: %v", err)
684+
}
685+
686+
// Change to temporary directory for the test
687+
origDir := getCurrentDir(t)
688+
tmpTestDir := createTempTestDir(t)
689+
defer func() {
690+
os.Chdir(origDir)
691+
os.RemoveAll(tmpTestDir)
692+
}()
693+
os.Chdir(tmpTestDir)
694+
695+
// Run RenderRulesToFormat for cursor
696+
err := RenderRulesToFormat(sourceDir, "cursor", false)
697+
if err != nil {
698+
t.Fatalf("RenderRulesToFormat failed: %v", err)
699+
}
700+
701+
// Verify file was created
702+
expectedFile := filepath.Join(".cursor", "rules", "test-rule.mdc")
703+
content, err := os.ReadFile(expectedFile)
704+
if err != nil {
705+
t.Fatalf("Failed to read generated file: %v", err)
706+
}
707+
708+
contentStr := string(content)
709+
710+
if tt.shouldApplyFallback {
711+
// Should have fallback frontmatter
712+
if !strings.Contains(contentStr, "alwaysApply: true") {
713+
t.Errorf("Expected fallback frontmatter to be applied.\nActual content:\n%s", contentStr)
714+
}
715+
if !strings.Contains(contentStr, "description:") {
716+
t.Errorf("Expected fallback frontmatter to contain 'description:' field")
717+
}
718+
if !strings.Contains(contentStr, "globs:") {
719+
t.Errorf("Expected fallback frontmatter to contain 'globs:' field")
720+
}
721+
} else {
722+
// Should not have fallback frontmatter, but should have the original relevant fields
723+
if strings.Contains(contentStr, "alwaysApply: true") && !strings.Contains(tt.frontmatter, "alwaysApply: true") {
724+
t.Errorf("Fallback frontmatter should not be applied when relevant fields exist")
725+
}
726+
}
727+
728+
// Verify body content is preserved
729+
if !strings.Contains(contentStr, "# Test Rule") {
730+
t.Errorf("Rule body content was not preserved")
731+
}
732+
733+
// If original frontmatter had irrelevant fields like 'name', they should be removed
734+
if tt.shouldApplyFallback && strings.Contains(contentStr, "name:") {
735+
t.Errorf("Irrelevant fields like 'name' should be removed in Cursor format")
736+
}
737+
})
738+
}
739+
}
740+
602741
// Helper functions
603742

604743
func createTempSourceDir(t *testing.T) string {

internal/formats/transform.go

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,8 @@ func TransformRuleContent(content []byte, format Format) ([]byte, error) {
7979

8080
var trimmedBodyContent = bytes.TrimSpace(bodyContent)
8181

82-
// Handle Cursor format fallback - if no frontmatter exists, use fallback format
83-
if format.Name == "cursor" && len(metadata) == 0 {
82+
// Handle Cursor format fallback - if no frontmatter exists or only irrelevant fields, use fallback format
83+
if format.Name == "cursor" && shouldApplyCursorFallback(metadata) {
8484
fallbackMetadata := RuleMetadata{
8585
"description": EmptyYAMLValue{},
8686
"globs": EmptyYAMLValue{},
@@ -133,6 +133,32 @@ func TransformRuleContent(content []byte, format Format) ([]byte, error) {
133133
return result.Bytes(), nil
134134
}
135135

136+
// shouldApplyCursorFallback determines if Cursor format should apply fallback frontmatter
137+
// Returns true if metadata is empty OR contains only irrelevant fields (name, tags, etc.)
138+
func shouldApplyCursorFallback(metadata RuleMetadata) bool {
139+
// If completely empty, apply fallback
140+
if len(metadata) == 0 {
141+
return true
142+
}
143+
144+
// Define relevant fields for Cursor format
145+
relevantFields := map[string]bool{
146+
"description": true,
147+
"alwaysApply": true,
148+
"globs": true,
149+
}
150+
151+
// Check if any relevant fields exist
152+
for key := range metadata {
153+
if relevantFields[key] {
154+
return false // Found a relevant field, don't apply fallback
155+
}
156+
}
157+
158+
// Only irrelevant fields found (like name, tags), apply fallback
159+
return true
160+
}
161+
136162
// cleanMetadataForYAML converts nil/empty values to EmptyYAMLValue for clean YAML output
137163
func cleanMetadataForYAML(metadata RuleMetadata) RuleMetadata {
138164
cleaned := RuleMetadata{}

0 commit comments

Comments
 (0)