@@ -505,4 +505,176 @@ func TestLintCommand_TwiceRule_TriggersOnSecondRun(t *testing.T) {
505505 if ! strings .Contains (out , "Twice Rule" ) {
506506 t .Errorf ("expected 'Twice Rule' in output on second run, got:\n %s" , out )
507507 }
508+ autoRulesPath := filepath .Join (histDir , "auto-rules.yaml" )
509+ data , err := os .ReadFile (autoRulesPath )
510+ if err != nil {
511+ t .Fatalf ("expected auto rules file to be created: %v" , err )
512+ }
513+ if ! strings .Contains (string (data ), "pkg/bare.go::Bare" ) {
514+ t .Fatalf ("expected triggered rule in auto rules file, got:\n %s" , string (data ))
515+ }
516+ }
517+
518+ func TestLintCommand_TwiceRule_DoesNotMutateManualConfig (t * testing.T ) {
519+ deps , stdout , stderr , db := setupLintTest (t )
520+
521+ db .Create (& model.Node {
522+ Namespace : ctxns .DefaultNamespace ,
523+ QualifiedName : "pkg/bare.go::Bare" ,
524+ Kind : model .NodeKindFunction ,
525+ Name : "Bare" ,
526+ FilePath : "pkg/bare.go" ,
527+ StartLine : 1 , EndLine : 5 ,
528+ Hash : "h1" , Language : "go" ,
529+ })
530+
531+ histDir := t .TempDir ()
532+ outDir := t .TempDir ()
533+ cfgFile := filepath .Join (t .TempDir (), ".ccg.yaml" )
534+ original := "exclude:\n - vendor\n "
535+ if err := os .WriteFile (cfgFile , []byte (original ), 0o644 ); err != nil {
536+ t .Fatal (err )
537+ }
538+
539+ if err := executeCmd (deps , stdout , stderr , "lint" , "--out" , outDir , "--config" , cfgFile , "--history-dir" , histDir ); err != nil {
540+ t .Fatalf ("first run unexpected error: %v" , err )
541+ }
542+ stdout .Reset ()
543+ stderr .Reset ()
544+
545+ if err := executeCmd (deps , stdout , stderr , "lint" , "--out" , outDir , "--config" , cfgFile , "--history-dir" , histDir ); err != nil {
546+ t .Fatalf ("second run unexpected error: %v" , err )
547+ }
548+
549+ data , err := os .ReadFile (cfgFile )
550+ if err != nil {
551+ t .Fatalf ("read config: %v" , err )
552+ }
553+ if string (data ) != original {
554+ t .Fatalf ("expected manual config to remain unchanged, got:\n %s" , string (data ))
555+ }
556+ }
557+
558+ func TestLintCommand_MergesAutoRulesIntoStrictEvaluation (t * testing.T ) {
559+ deps , stdout , stderr , db := setupLintTest (t )
560+
561+ db .Create (& model.Node {
562+ Namespace : ctxns .DefaultNamespace ,
563+ QualifiedName : "pkg/ignored.go::Ignored" ,
564+ Kind : model .NodeKindFunction ,
565+ Name : "Ignored" ,
566+ FilePath : "pkg/ignored.go" ,
567+ StartLine : 1 , EndLine : 5 ,
568+ Hash : "h1" , Language : "go" ,
569+ })
570+
571+ outDir := t .TempDir ()
572+ histDir := t .TempDir ()
573+ docDir := filepath .Join (outDir , "pkg" )
574+ if err := os .MkdirAll (docDir , 0o755 ); err != nil {
575+ t .Fatal (err )
576+ }
577+ if err := os .WriteFile (filepath .Join (docDir , "ignored.go.md" ), []byte ("# pkg/ignored.go\n " ), 0o644 ); err != nil {
578+ t .Fatal (err )
579+ }
580+ if err := os .WriteFile (filepath .Join (histDir , "auto-rules.yaml" ), []byte (`rules:
581+ - pattern: "pkg/ignored.go::Ignored"
582+ category: unannotated
583+ action: ignore
584+ auto: true
585+ created: "2026-05-03"
586+ ` ), 0o644 ); err != nil {
587+ t .Fatal (err )
588+ }
589+
590+ err := executeCmd (deps , stdout , stderr , "lint" , "--out" , outDir , "--strict" , "--history-dir" , histDir )
591+ if err != nil {
592+ t .Fatalf ("expected auto-rules ignore to be merged into strict evaluation, got: %v" , err )
593+ }
594+ }
595+
596+ func TestLintCommand_MigrateAutoRules_MovesGeneratedRulesOutOfConfig (t * testing.T ) {
597+ deps , stdout , stderr , _ := setupLintTest (t )
598+ histDir := t .TempDir ()
599+ cfgFile := filepath .Join (t .TempDir (), ".ccg.yaml" )
600+ original := `exclude:
601+ - vendor
602+ rules:
603+ - pattern: "pkg/manual.go::Keep"
604+ category: unannotated
605+ action: ignore
606+ auto: false
607+ created: "2026-05-03"
608+ - pattern: "pkg/auto.go::Move"
609+ category: unannotated
610+ action: warn
611+ auto: true
612+ created: "2026-05-03"
613+ `
614+ if err := os .WriteFile (cfgFile , []byte (original ), 0o644 ); err != nil {
615+ t .Fatal (err )
616+ }
617+
618+ err := executeCmd (deps , stdout , stderr , "lint" , "--config" , cfgFile , "--history-dir" , histDir , "--migrate-auto-rules" )
619+ if err != nil {
620+ t .Fatalf ("expected migrate-auto-rules to succeed, got: %v" , err )
621+ }
622+
623+ configData , err := os .ReadFile (cfgFile )
624+ if err != nil {
625+ t .Fatalf ("read config: %v" , err )
626+ }
627+ configText := string (configData )
628+ if strings .Contains (configText , "pkg/auto.go::Move" ) {
629+ t .Fatalf ("expected auto rule to be removed from config, got:\n %s" , configText )
630+ }
631+ if ! strings .Contains (configText , "pkg/manual.go::Keep" ) {
632+ t .Fatalf ("expected manual rule to remain in config, got:\n %s" , configText )
633+ }
634+
635+ autoData , err := os .ReadFile (filepath .Join (histDir , "auto-rules.yaml" ))
636+ if err != nil {
637+ t .Fatalf ("expected auto-rules.yaml to be created: %v" , err )
638+ }
639+ autoText := string (autoData )
640+ if ! strings .Contains (autoText , "pkg/auto.go::Move" ) {
641+ t .Fatalf ("expected migrated auto rule in generated state, got:\n %s" , autoText )
642+ }
643+ if strings .Contains (autoText , "pkg/manual.go::Keep" ) {
644+ t .Fatalf ("expected manual rule not to be copied into generated state, got:\n %s" , autoText )
645+ }
646+ }
647+
648+ func TestLintCommand_MigrateAutoRules_PreservesNonRuleConfig (t * testing.T ) {
649+ deps , stdout , stderr , _ := setupLintTest (t )
650+ histDir := t .TempDir ()
651+ cfgFile := filepath .Join (t .TempDir (), ".ccg.yaml" )
652+ config := `exclude:
653+ - vendor
654+ docs:
655+ out: docs
656+ rules:
657+ - pattern: "pkg/auto.go::Move"
658+ category: unannotated
659+ action: warn
660+ auto: true
661+ created: "2026-05-03"
662+ `
663+ if err := os .WriteFile (cfgFile , []byte (config ), 0o644 ); err != nil {
664+ t .Fatal (err )
665+ }
666+
667+ err := executeCmd (deps , stdout , stderr , "lint" , "--config" , cfgFile , "--history-dir" , histDir , "--migrate-auto-rules" )
668+ if err != nil {
669+ t .Fatalf ("expected migrate-auto-rules to succeed, got: %v" , err )
670+ }
671+
672+ data , err := os .ReadFile (cfgFile )
673+ if err != nil {
674+ t .Fatalf ("read config: %v" , err )
675+ }
676+ text := string (data )
677+ if ! strings .Contains (text , "exclude:" ) || ! strings .Contains (text , "docs:" ) || ! strings .Contains (text , "out: docs" ) {
678+ t .Fatalf ("expected non-rule config to be preserved, got:\n %s" , text )
679+ }
508680}
0 commit comments