@@ -5,195 +5,152 @@ import { afterEach, beforeEach, describe, expect, it } from 'vite-plus/test';
55
66import { collectRuleFiles , parseFrontmatter , walkDir } from '../utils.js' ;
77
8- // ─── Helpers ────────────────────────────────────────────────────────────────
8+ // ─── Helpers ─────────────────────────────────────────────────────────────────
99
10- function tmpDir ( ) : string {
11- return fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'ndns-test-' ) ) ;
12- }
10+ let tmpRoot : string ;
1311
14- function write ( dir : string , relPath : string , content = '' ) : string {
15- const full = path . join ( dir , relPath ) ;
12+ function write ( rel : string , content = '' ) : string {
13+ const full = path . join ( tmpRoot , rel ) ;
1614 fs . mkdirSync ( path . dirname ( full ) , { recursive : true } ) ;
1715 fs . writeFileSync ( full , content , 'utf8' ) ;
1816 return full ;
1917}
2018
21- // ─── walkDir ────────────────────────────────────────────────────────────────
19+ beforeEach ( ( ) => {
20+ tmpRoot = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'ndns-utils-' ) ) ;
21+ } ) ;
22+
23+ afterEach ( ( ) => {
24+ fs . rmSync ( tmpRoot , { recursive : true , force : true } ) ;
25+ } ) ;
26+
27+ // ─── walkDir ─────────────────────────────────────────────────────────────────
2228
2329describe ( 'walkDir' , ( ) => {
24- let dir : string ;
25- beforeEach ( ( ) => {
26- dir = tmpDir ( ) ;
27- } ) ;
28- afterEach ( ( ) => {
29- fs . rmSync ( dir , { recursive : true , force : true } ) ;
30+ it ( 'returns empty array for non-existent directory' , ( ) => {
31+ expect ( walkDir ( path . join ( tmpRoot , 'missing' ) , ( ) => true ) ) . toEqual ( [ ] ) ;
3032 } ) ;
3133
3234 it ( 'returns empty array for empty directory' , ( ) => {
33- expect ( walkDir ( dir , ( ) => true ) ) . toEqual ( [ ] ) ;
34- } ) ;
35-
36- it ( 'returns empty array for non-existent directory' , ( ) => {
37- expect ( walkDir ( '/does/not/exist' , ( ) => true ) ) . toEqual ( [ ] ) ;
35+ fs . mkdirSync ( path . join ( tmpRoot , 'empty' ) ) ;
36+ expect ( walkDir ( path . join ( tmpRoot , 'empty' ) , ( ) => true ) ) . toEqual ( [ ] ) ;
3837 } ) ;
3938
40- it ( 'collects files matching the predicate' , ( ) => {
41- write ( dir , 'a .md') ;
42- write ( dir , 'b.ts ') ;
43- write ( dir , 'c.md ') ;
44- const results = walkDir ( dir , ( n ) => n . endsWith ( '.md' ) ) ;
39+ it ( 'collects files matching predicate' , ( ) => {
40+ write ( 'a/one .md') ;
41+ write ( 'a/two.md ') ;
42+ write ( 'a/skip.txt ') ;
43+ const results = walkDir ( path . join ( tmpRoot , 'a' ) , ( n ) => n . endsWith ( '.md' ) ) ;
4544 expect ( results ) . toHaveLength ( 2 ) ;
4645 expect ( results . every ( ( f ) => f . endsWith ( '.md' ) ) ) . toBe ( true ) ;
4746 } ) ;
4847
4948 it ( 'recurses into subdirectories' , ( ) => {
50- write ( dir , 'sub/deep/rule.md' ) ;
51- write ( dir , 'root.md' ) ;
52- const results = walkDir ( dir , ( n ) => n . endsWith ( '.md' ) ) ;
53- expect ( results ) . toHaveLength ( 2 ) ;
49+ write ( 'root/sub1/a.md' ) ;
50+ write ( 'root/sub1/sub2/b.md' ) ;
51+ write ( 'root/c.md' ) ;
52+ const results = walkDir ( path . join ( tmpRoot , 'root' ) , ( n ) => n . endsWith ( '.md' ) ) ;
53+ expect ( results ) . toHaveLength ( 3 ) ;
5454 } ) ;
5555
56- it ( 'does not return files that do not match predicate' , ( ) => {
57- write ( dir , 'file.json' ) ;
58- const results = walkDir ( dir , ( n ) => n . endsWith ( '.md' ) ) ;
59- expect ( results ) . toHaveLength ( 0 ) ;
56+ it ( 'excludes files that do not match predicate' , ( ) => {
57+ write ( 'dir/keep.ts' ) ;
58+ write ( 'dir/skip.md' ) ;
59+ const results = walkDir ( path . join ( tmpRoot , 'dir' ) , ( n ) => n . endsWith ( '.ts' ) ) ;
60+ expect ( results ) . toHaveLength ( 1 ) ;
61+ expect ( results [ 0 ] ) . toMatch ( / k e e p \. t s $ / ) ;
6062 } ) ;
6163} ) ;
6264
63- // ─── parseFrontmatter ───────────────────────────────────────────────────────
65+ // ─── parseFrontmatter ─────────────────────────────────────────────────────────
6466
6567describe ( 'parseFrontmatter' , ( ) => {
6668 it ( 'returns empty object when no frontmatter' , ( ) => {
67- expect ( parseFrontmatter ( '# Hello\nNo frontmatter here. ' ) ) . toEqual ( { } ) ;
69+ expect ( parseFrontmatter ( '# No frontmatter here' ) ) . toEqual ( { } ) ;
6870 } ) ;
6971
70- it ( 'returns empty object when content does not start with --- ' , ( ) => {
71- expect ( parseFrontmatter ( 'title: foo\n--- ' ) ) . toEqual ( { } ) ;
72+ it ( 'returns empty object when frontmatter is not closed ' , ( ) => {
73+ expect ( parseFrontmatter ( '---\ntitle: Test\n ' ) ) . toEqual ( { } ) ;
7274 } ) ;
7375
74- it ( 'parses scalar string fields' , ( ) => {
75- const md = `---
76- title: 'Authentication'
77- impact: HIGH
78- type: capability
79- ---
80- # Body` ;
81- const fm = parseFrontmatter ( md ) ;
82- expect ( fm [ 'title' ] ) . toBe ( 'Authentication' ) ;
76+ it ( 'parses simple string fields' , ( ) => {
77+ const fm = parseFrontmatter ( '---\ntitle: Hello\nimpact: HIGH\n---\n' ) ;
78+ expect ( fm [ 'title' ] ) . toBe ( 'Hello' ) ;
8379 expect ( fm [ 'impact' ] ) . toBe ( 'HIGH' ) ;
84- expect ( fm [ 'type' ] ) . toBe ( 'capability' ) ;
85- } ) ;
86-
87- it ( 'parses YAML array fields' , ( ) => {
88- const md = `---
89- tags:
90- - api
91- - security
92- - authentication
93- ---` ;
94- const fm = parseFrontmatter ( md ) ;
95- expect ( fm [ 'tags' ] ) . toEqual ( [ 'api' , 'security' , 'authentication' ] ) ;
9680 } ) ;
9781
9882 it ( 'strips surrounding quotes from values' , ( ) => {
99- const md = `---
100- title: "Rate Limiting"
101- impactDescription: 'Some description'
102- ---` ;
103- const fm = parseFrontmatter ( md ) ;
104- expect ( fm [ 'title' ] ) . toBe ( 'Rate Limiting' ) ;
105- expect ( fm [ 'impactDescription' ] ) . toBe ( 'Some description' ) ;
106- } ) ;
107-
108- it ( 'handles multiple fields including mixed scalar and array' , ( ) => {
109- const md = `---
110- title: 'My Rule'
111- impact: MEDIUM
112- tags:
113- - one
114- - two
115- type: efficiency
116- ---` ;
117- const fm = parseFrontmatter ( md ) ;
118- expect ( fm [ 'title' ] ) . toBe ( 'My Rule' ) ;
119- expect ( fm [ 'impact' ] ) . toBe ( 'MEDIUM' ) ;
120- expect ( fm [ 'tags' ] ) . toEqual ( [ 'one' , 'two' ] ) ;
121- expect ( fm [ 'type' ] ) . toBe ( 'efficiency' ) ;
83+ const fm = parseFrontmatter ( "---\ntitle: 'Quoted Title'\n---\n" ) ;
84+ expect ( fm [ 'title' ] ) . toBe ( 'Quoted Title' ) ;
12285 } ) ;
12386
124- it ( 'returns empty object when closing --- is missing' , ( ) => {
125- const md = `---
126- title: Missing close
127- ` ;
128- expect ( parseFrontmatter ( md ) ) . toEqual ( { } ) ;
87+ it ( 'parses YAML array into string[]' , ( ) => {
88+ const fm = parseFrontmatter ( '---\ntags:\n - dns\n - api\n - setup\n---\n' ) ;
89+ expect ( fm [ 'tags' ] ) . toEqual ( [ 'dns' , 'api' , 'setup' ] ) ;
12990 } ) ;
130- } ) ;
13191
132- // ─── collectRuleFiles ────────────────────────────────────────────────────────
133-
134- describe ( 'collectRuleFiles' , ( ) => {
135- let dir : string ;
136- beforeEach ( ( ) => {
137- dir = tmpDir ( ) ;
138- } ) ;
139- afterEach ( ( ) => {
140- fs . rmSync ( dir , { recursive : true , force : true } ) ;
92+ it ( 'returns empty array for empty YAML array' , ( ) => {
93+ const fm = parseFrontmatter ( '---\ntags:\n---\n' ) ;
94+ expect ( fm [ 'tags' ] ) . toEqual ( [ ] ) ;
14195 } ) ;
14296
143- it ( 'returns empty array for empty directory' , ( ) => {
144- expect ( collectRuleFiles ( dir ) ) . toEqual ( [ ] ) ;
97+ it ( 'handles multiple fields and arrays' , ( ) => {
98+ const content = [
99+ '---' ,
100+ "title: 'My Rule'" ,
101+ 'impact: MEDIUM' ,
102+ 'type: capability' ,
103+ 'tags:' ,
104+ ' - cli' ,
105+ ' - dns' ,
106+ '---' ,
107+ '# Body' ,
108+ ] . join ( '\n' ) ;
109+ const fm = parseFrontmatter ( content ) ;
110+ expect ( fm [ 'title' ] ) . toBe ( 'My Rule' ) ;
111+ expect ( fm [ 'impact' ] ) . toBe ( 'MEDIUM' ) ;
112+ expect ( fm [ 'tags' ] ) . toEqual ( [ 'cli' , 'dns' ] ) ;
145113 } ) ;
146114
147- it ( 'collects .md files ' , ( ) => {
148- write ( dir , 'rule-one.md' ) ;
149- write ( dir , 'rule-two.md' ) ;
150- expect ( collectRuleFiles ( dir ) ) . toHaveLength ( 2 ) ;
115+ it ( 'handles multiline impactDescription ' , ( ) => {
116+ const content = "---\ntitle: 'Test'\nimpactDescription: 'A long description'\n---\n" ;
117+ const fm = parseFrontmatter ( content ) ;
118+ expect ( fm [ 'impactDescription' ] ) . toBe ( 'A long description' ) ;
151119 } ) ;
120+ } ) ;
152121
153- it ( 'excludes files starting with underscore' , ( ) => {
154- write ( dir , '_draft.md' ) ;
155- write ( dir , 'real.md' ) ;
156- const results = collectRuleFiles ( dir ) ;
157- expect ( results ) . toHaveLength ( 1 ) ;
158- expect ( results [ 0 ] ) . toContain ( 'real.md' ) ;
159- } ) ;
122+ // ─── collectRuleFiles ─────────────────────────────────────────────────────────
160123
161- it ( 'excludes README.md ', ( ) => {
162- write ( dir , 'README .md') ;
163- write ( dir , 'actual-rule .md') ;
164- const results = collectRuleFiles ( dir ) ;
165- expect ( results ) . toHaveLength ( 1 ) ;
166- expect ( results [ 0 ] ) . toContain ( 'actual-rule .md') ;
124+ describe ( 'collectRuleFiles ', ( ) => {
125+ it ( 'excludes SKILL .md', ( ) => {
126+ write ( 'rules/SKILL .md') ;
127+ write ( 'rules/auth.md' ) ;
128+ const results = collectRuleFiles ( path . join ( tmpRoot , 'rules' ) ) ;
129+ expect ( results . every ( ( f ) => ! f . endsWith ( 'SKILL .md') ) ) . toBe ( true ) ;
167130 } ) ;
168131
169- it ( 'excludes SKILL .md' , ( ) => {
170- write ( dir , 'SKILL .md') ;
171- write ( dir , 'rule .md') ;
172- const results = collectRuleFiles ( dir ) ;
173- expect ( results ) . toHaveLength ( 1 ) ;
132+ it ( 'excludes README .md' , ( ) => {
133+ write ( 'rules/README .md') ;
134+ write ( 'rules/setup .md') ;
135+ const results = collectRuleFiles ( path . join ( tmpRoot , 'rules' ) ) ;
136+ expect ( results . every ( ( f ) => ! f . endsWith ( 'README.md' ) ) ) . toBe ( true ) ;
174137 } ) ;
175138
176- it ( 'excludes non-.md files' , ( ) => {
177- write ( dir , 'script.ts ') ;
178- write ( dir , 'config.json ') ;
179- write ( dir , 'rule.md' ) ;
180- expect ( collectRuleFiles ( dir ) ) . toHaveLength ( 1 ) ;
139+ it ( 'excludes files starting with underscore ' , ( ) => {
140+ write ( 'rules/_draft.md ') ;
141+ write ( 'rules/published.md ') ;
142+ const results = collectRuleFiles ( path . join ( tmpRoot , 'rules' ) ) ;
143+ expect ( results . every ( ( f ) => ! path . basename ( f ) . startsWith ( '_' ) ) ) . toBe ( true ) ;
181144 } ) ;
182145
183- it ( 'recurses into subdirectories' , ( ) => {
184- write ( dir , 'nuxt/api-proxy.md' ) ;
185- write ( dir , 'nextjs/api-proxy.md' ) ;
186- write ( dir , 'top-level.md' ) ;
187- const results = collectRuleFiles ( dir ) ;
188- expect ( results ) . toHaveLength ( 3 ) ;
146+ it ( 'includes normal .md files in subdirs' , ( ) => {
147+ write ( 'rules/nuxt/api-proxy.md' ) ;
148+ write ( 'rules/nextjs/error-handling.md' ) ;
149+ const results = collectRuleFiles ( path . join ( tmpRoot , 'rules' ) ) ;
150+ expect ( results ) . toHaveLength ( 2 ) ;
189151 } ) ;
190152
191- it ( 'returns sorted file paths' , ( ) => {
192- write ( dir , 'zzz.md' ) ;
193- write ( dir , 'aaa.md' ) ;
194- write ( dir , 'mmm.md' ) ;
195- const results = collectRuleFiles ( dir ) ;
196- expect ( results [ 0 ] ) . toContain ( 'aaa.md' ) ;
197- expect ( results [ 2 ] ) . toContain ( 'zzz.md' ) ;
153+ it ( 'returns empty array for non-existent directory' , ( ) => {
154+ expect ( collectRuleFiles ( path . join ( tmpRoot , 'missing' ) ) ) . toEqual ( [ ] ) ;
198155 } ) ;
199156} ) ;
0 commit comments