@@ -3,6 +3,7 @@ import type {
33 GlobalMemoryPrompt ,
44 OutputPluginContext ,
55 OutputWriteContext ,
6+ RulePrompt ,
67 SkillPrompt
78} from '@/types'
89import type { RelativePath } from '@/types/FileSystemTypes'
@@ -11,7 +12,7 @@ import * as os from 'node:os'
1112import * as path from 'node:path'
1213import { afterEach , beforeEach , describe , expect , it } from 'vitest'
1314import { createLogger } from '@/log'
14- import { FilePathKind , PromptKind } from '@/types'
15+ import { FilePathKind , NamingCaseKind , PromptKind } from '@/types'
1516import { WindsurfOutputPlugin } from './WindsurfOutputPlugin'
1617
1718function createMockRelativePath ( pathStr : string , basePath : string ) : RelativePath {
@@ -24,14 +25,46 @@ function createMockRelativePath(pathStr: string, basePath: string): RelativePath
2425 }
2526}
2627
28+ function createMockRulePrompt (
29+ series : string ,
30+ ruleName : string ,
31+ globs : readonly string [ ] ,
32+ scope : 'global' | 'project' ,
33+ seriName ?: string
34+ ) : RulePrompt {
35+ const content = '# Rule body\n\nFollow this rule.'
36+ return {
37+ type : PromptKind . Rule ,
38+ content,
39+ length : content . length ,
40+ filePathKind : FilePathKind . Relative ,
41+ dir : createMockRelativePath ( '.' , '' ) ,
42+ markdownContents : [ ] ,
43+ yamlFrontMatter : {
44+ description : 'Rule description' ,
45+ globs,
46+ namingCase : NamingCaseKind . KebabCase
47+ } ,
48+ series,
49+ ruleName,
50+ globs,
51+ scope,
52+ ...seriName != null && { seriName}
53+ } as RulePrompt
54+ }
55+
2756function createMockGlobalMemoryPrompt ( content : string , basePath : string ) : GlobalMemoryPrompt {
2857 return {
2958 type : PromptKind . GlobalMemory ,
3059 content,
3160 length : content . length ,
3261 filePathKind : FilePathKind . Relative ,
3362 dir : createMockRelativePath ( '.' , basePath ) ,
34- markdownContents : [ ]
63+ markdownContents : [ ] ,
64+ parentDirectoryPath : {
65+ type : 'UserHome' ,
66+ directory : createMockRelativePath ( '.codeium/windsurf' , basePath )
67+ }
3568 } as GlobalMemoryPrompt
3669}
3770
@@ -48,7 +81,7 @@ function createMockFastCommandPrompt(
4881 filePathKind : FilePathKind . Relative ,
4982 dir : createMockRelativePath ( '.' , basePath ) ,
5083 markdownContents : [ ] ,
51- yamlFrontMatter : { description : 'Fast command' } ,
84+ yamlFrontMatter : { description : 'Fast command' , namingCase : NamingCaseKind . KebabCase } ,
5285 ...series != null && { series} ,
5386 commandName
5487 } as FastCommandPrompt
@@ -61,15 +94,15 @@ function createMockSkillPrompt(
6194 options ?: { childDocs ?: { relativePath : string , content : unknown } [ ] , resources ?: { relativePath : string , content : string , encoding : 'text' | 'base64' } [ ] }
6295) : SkillPrompt {
6396 return {
64- yamlFrontMatter : { name, description : 'A skill' } ,
97+ yamlFrontMatter : { name, description : 'A skill' , namingCase : NamingCaseKind . KebabCase } ,
6598 dir : createMockRelativePath ( name , basePath ) ,
6699 content,
67100 length : content . length ,
68101 type : PromptKind . Skill ,
69102 filePathKind : FilePathKind . Relative ,
70103 markdownContents : [ ] ,
71104 ...options
72- } as SkillPrompt
105+ } as unknown as SkillPrompt
73106}
74107
75108class TestableWindsurfOutputPlugin extends WindsurfOutputPlugin {
@@ -134,8 +167,8 @@ describe('windsurf output plugin', () => {
134167
135168 const results = await plugin . registerGlobalOutputDirs ( ctx )
136169 expect ( results ) . toHaveLength ( 1 )
137- expect ( results [ 0 ] . path ) . toBe ( 'global_workflows' )
138- expect ( results [ 0 ] . getAbsolutePath ( ) ) . toBe ( path . join ( tempDir , '.codeium' , 'windsurf' , 'global_workflows' ) )
170+ expect ( results [ 0 ] ? .path ) . toBe ( 'global_workflows' )
171+ expect ( results [ 0 ] ? .getAbsolutePath ( ) ) . toBe ( path . join ( tempDir , '.codeium' , 'windsurf' , 'global_workflows' ) )
139172 } )
140173
141174 it ( 'should register skills/<skillName> dir when skills exist' , async ( ) => {
@@ -148,8 +181,8 @@ describe('windsurf output plugin', () => {
148181
149182 const results = await plugin . registerGlobalOutputDirs ( ctx )
150183 expect ( results ) . toHaveLength ( 1 )
151- expect ( results [ 0 ] . path ) . toBe ( path . join ( 'skills' , 'custom-skill' ) )
152- expect ( results [ 0 ] . getAbsolutePath ( ) ) . toBe ( path . join ( tempDir , '.codeium' , 'windsurf' , 'skills' , 'custom-skill' ) )
184+ expect ( results [ 0 ] ? .path ) . toBe ( path . join ( 'skills' , 'custom-skill' ) )
185+ expect ( results [ 0 ] ? .getAbsolutePath ( ) ) . toBe ( path . join ( tempDir , '.codeium' , 'windsurf' , 'skills' , 'custom-skill' ) )
153186 } )
154187
155188 it ( 'should register both workflows and skills dirs when both exist' , async ( ) => {
@@ -320,7 +353,7 @@ describe('windsurf output plugin', () => {
320353
321354 const results = await plugin . writeGlobalOutputs ( ctx )
322355 expect ( results . files . length ) . toBeGreaterThanOrEqual ( 1 )
323- expect ( results . files [ 0 ] . success ) . toBe ( true )
356+ expect ( results . files [ 0 ] ? .success ) . toBe ( true )
324357
325358 const memoryPath = path . join ( tempDir , '.codeium' , 'windsurf' , 'memories' , 'global_rules.md' )
326359 expect ( fs . existsSync ( memoryPath ) ) . toBe ( true )
@@ -442,15 +475,42 @@ describe('windsurf output plugin', () => {
442475
443476 const results = await plugin . writeGlobalOutputs ( ctx )
444477 expect ( results . files . length ) . toBeGreaterThanOrEqual ( 1 )
445- expect ( results . files [ 0 ] . success ) . toBe ( true )
478+ expect ( results . files [ 0 ] ? .success ) . toBe ( true )
446479
447480 const memoryPath = path . join ( tempDir , '.codeium' , 'windsurf' , 'memories' , 'global_rules.md' )
448481 expect ( fs . existsSync ( memoryPath ) ) . toBe ( false )
449482 } )
483+
484+ it ( 'should write global rule files with trigger/globs frontmatter' , async ( ) => {
485+ const ctx = {
486+ collectedInputContext : {
487+ workspace : { projects : [ ] , directory : createMockRelativePath ( '.' , tempDir ) } ,
488+ skills : [ ] ,
489+ fastCommands : [ ] ,
490+ rules : [
491+ createMockRulePrompt ( 'test' , 'glob' , [ 'src/**/*.ts' , '**/*.tsx' ] , 'global' )
492+ ]
493+ } ,
494+ logger : createLogger ( 'test' , 'debug' ) ,
495+ dryRun : false
496+ } as unknown as OutputWriteContext
497+
498+ const results = await plugin . writeGlobalOutputs ( ctx )
499+ expect ( results . files ) . toHaveLength ( 1 )
500+
501+ const rulePath = path . join ( tempDir , '.codeium' , 'windsurf' , 'memories' , 'rule-test-glob.md' )
502+ expect ( fs . existsSync ( rulePath ) ) . toBe ( true )
503+
504+ const content = fs . readFileSync ( rulePath , 'utf8' )
505+ expect ( content ) . toContain ( 'trigger: glob' )
506+ expect ( content ) . toContain ( 'globs: src/**/*.ts, **/*.tsx' )
507+ expect ( content ) . not . toContain ( 'globs: "src/**/*.ts, **/*.tsx"' )
508+ expect ( content ) . toContain ( 'Follow this rule.' )
509+ } )
450510 } )
451511
452512 describe ( 'writeProjectOutputs' , ( ) => {
453- it ( 'should return empty results ( no project outputs) ' , async ( ) => {
513+ it ( 'should return empty results when no project rules ' , async ( ) => {
454514 const ctx = {
455515 collectedInputContext : {
456516 workspace : { projects : [ ] , directory : createMockRelativePath ( '.' , tempDir ) } ,
@@ -464,6 +524,45 @@ describe('windsurf output plugin', () => {
464524 expect ( results . files ) . toHaveLength ( 0 )
465525 expect ( results . dirs ) . toHaveLength ( 0 )
466526 } )
527+
528+ it ( 'should write project rules and apply seriName include filter from projectConfig' , async ( ) => {
529+ const ctx = {
530+ collectedInputContext : {
531+ workspace : {
532+ projects : [
533+ {
534+ name : 'proj1' ,
535+ dirFromWorkspacePath : createMockRelativePath ( 'proj1' , tempDir ) ,
536+ projectConfig : { rules : { include : [ 'uniapp' ] } }
537+ }
538+ ] ,
539+ directory : createMockRelativePath ( '.' , tempDir )
540+ } ,
541+ rules : [
542+ createMockRulePrompt ( 'test' , 'uniapp-only' , [ 'src/**/*.vue' ] , 'project' , 'uniapp' ) ,
543+ createMockRulePrompt ( 'test' , 'vue-only' , [ 'src/**/*.ts' ] , 'project' , 'vue' )
544+ ]
545+ } ,
546+ logger : createLogger ( 'test' , 'debug' ) ,
547+ dryRun : false
548+ } as unknown as OutputWriteContext
549+
550+ const results = await plugin . writeProjectOutputs ( ctx )
551+ const outputPaths = results . files . map ( file => file . path . path . replaceAll ( '\\' , '/' ) )
552+
553+ expect ( outputPaths . some ( p => p . endsWith ( 'rule-test-uniapp-only.md' ) ) ) . toBe ( true )
554+ expect ( outputPaths . some ( p => p . endsWith ( 'rule-test-vue-only.md' ) ) ) . toBe ( false )
555+
556+ const includedRulePath = path . join ( tempDir , 'proj1' , '.windsurf' , 'rules' , 'rule-test-uniapp-only.md' )
557+ const excludedRulePath = path . join ( tempDir , 'proj1' , '.windsurf' , 'rules' , 'rule-test-vue-only.md' )
558+
559+ expect ( fs . existsSync ( includedRulePath ) ) . toBe ( true )
560+ expect ( fs . existsSync ( excludedRulePath ) ) . toBe ( false )
561+
562+ const includedRuleContent = fs . readFileSync ( includedRulePath , 'utf8' )
563+ expect ( includedRuleContent ) . toContain ( 'trigger: glob' )
564+ expect ( includedRuleContent ) . toContain ( 'globs: src/**/*.vue' )
565+ } )
467566 } )
468567
469568 describe ( 'clean support' , ( ) => {
0 commit comments