@@ -3,8 +3,9 @@ import { tmpdir } from "node:os";
33import path from "node:path" ;
44import type { Hunk , PullRequestFile } from "@stagereview/types/parsed-diff" ;
55import { LINE_TYPE } from "@stagereview/types/parsed-diff" ;
6+ import ignore from "ignore" ;
67import { describe , expect , it } from "vitest" ;
7- import { filterFilesForLlm , loadStageIgnorePatterns , shouldIncludeFile } from "../filter-files.js" ;
8+ import { filterFilesForLlm , loadStageIgnore , shouldIncludeFile } from "../filter-files.js" ;
89
910function makeHunk ( lineCount : number , overrides ?: Partial < Hunk > ) : Hunk {
1011 return {
@@ -35,6 +36,10 @@ function makeFile(overrides?: Partial<PullRequestFile>): PullRequestFile {
3536 } ;
3637}
3738
39+ function ig ( patterns : string [ ] ) {
40+ return ignore ( ) . add ( patterns ) ;
41+ }
42+
3843describe ( "shouldIncludeFile" , ( ) => {
3944 const denylistedFilenames = [
4045 "package-lock.json" ,
@@ -144,7 +149,7 @@ describe("filterFilesForLlm", () => {
144149 makeFile ( { path : "build/config.gypi" } ) ,
145150 makeFile ( { path : "dist/bundle.js" } ) ,
146151 ] ;
147- const result = filterFilesForLlm ( files , [ "build/**" , "dist/**" ] ) ;
152+ const result = filterFilesForLlm ( files , ig ( [ "build/**" , "dist/**" ] ) ) ;
148153 expect ( result . files ) . toHaveLength ( 1 ) ;
149154 expect ( result . files [ 0 ] ?. path ) . toBe ( "src/app.ts" ) ;
150155 expect ( result . excludedByPath ) . toEqual ( [ "build/config.gypi" , "dist/bundle.js" ] ) ;
@@ -156,32 +161,32 @@ describe("filterFilesForLlm", () => {
156161 makeFile ( { path : "pnpm-lock.yaml" } ) ,
157162 makeFile ( { path : "generated/schema.ts" } ) ,
158163 ] ;
159- const result = filterFilesForLlm ( files , [ "generated/**" ] ) ;
164+ const result = filterFilesForLlm ( files , ig ( [ "generated/**" ] ) ) ;
160165 expect ( result . files ) . toHaveLength ( 1 ) ;
161166 expect ( result . files [ 0 ] ?. path ) . toBe ( "src/app.ts" ) ;
162167 expect ( result . excludedByPath ) . toEqual ( [ "pnpm-lock.yaml" , "generated/schema.ts" ] ) ;
163168 } ) ;
164169
165- it ( "works normally when stageIgnorePatterns is undefined" , ( ) => {
170+ it ( "works normally when stageIgnore is undefined" , ( ) => {
166171 const files = [ makeFile ( { path : "src/app.ts" } ) , makeFile ( { path : "pnpm-lock.yaml" } ) ] ;
167172 const result = filterFilesForLlm ( files , undefined ) ;
168173 expect ( result . files ) . toHaveLength ( 1 ) ;
169174 expect ( result . files [ 0 ] ?. path ) . toBe ( "src/app.ts" ) ;
170175 } ) ;
171176
172- it ( "works normally when stageIgnorePatterns is empty " , ( ) => {
177+ it ( "works normally when stageIgnore is null " , ( ) => {
173178 const files = [ makeFile ( { path : "src/app.ts" } ) , makeFile ( { path : "src/utils.ts" } ) ] ;
174- const result = filterFilesForLlm ( files , [ ] ) ;
179+ const result = filterFilesForLlm ( files , null ) ;
175180 expect ( result . files ) . toHaveLength ( 2 ) ;
176181 } ) ;
177182
178- it ( "matches slashless globs against nested paths via matchBase " , ( ) => {
183+ it ( "slashless globs match nested paths" , ( ) => {
179184 const files = [
180185 makeFile ( { path : "src/app.ts" } ) ,
181186 makeFile ( { path : "src/schema.generated.ts" } ) ,
182187 makeFile ( { path : "lib/deep/nested/types.generated.ts" } ) ,
183188 ] ;
184- const result = filterFilesForLlm ( files , [ "*.generated.ts" ] ) ;
189+ const result = filterFilesForLlm ( files , ig ( [ "*.generated.ts" ] ) ) ;
185190 expect ( result . files ) . toHaveLength ( 1 ) ;
186191 expect ( result . files [ 0 ] ?. path ) . toBe ( "src/app.ts" ) ;
187192 } ) ;
@@ -192,35 +197,34 @@ describe("filterFilesForLlm", () => {
192197 makeFile ( { path : "build/important.js" } ) ,
193198 makeFile ( { path : "src/app.ts" } ) ,
194199 ] ;
195- const result = filterFilesForLlm ( files , [ "build/**" , "!build/important.js" ] ) ;
200+ const result = filterFilesForLlm ( files , ig ( [ "build/**" , "!build/important.js" ] ) ) ;
196201 expect ( result . files ) . toHaveLength ( 2 ) ;
197202 expect ( result . files . map ( ( f ) => f . path ) ) . toEqual ( [ "build/important.js" , "src/app.ts" ] ) ;
198203 } ) ;
199204
200205 it ( "last matching pattern wins with negation" , ( ) => {
201206 const files = [ makeFile ( { path : "dist/bundle.js" } ) ] ;
202- // exclude, re-include, exclude again
203- const result = filterFilesForLlm ( files , [ "dist/**" , "!dist/bundle.js" , "*.js" ] ) ;
207+ const result = filterFilesForLlm ( files , ig ( [ "dist/**" , "!dist/bundle.js" , "*.js" ] ) ) ;
204208 expect ( result . files ) . toHaveLength ( 0 ) ;
205209 } ) ;
206210
207- it ( "strips leading slash from root-anchored patterns " , ( ) => {
211+ it ( "leading slash anchors a pattern to the repo root " , ( ) => {
208212 const files = [ makeFile ( { path : "dist/bundle.js" } ) , makeFile ( { path : "src/app.ts" } ) ] ;
209- const result = filterFilesForLlm ( files , [ "/dist/**" ] ) ;
213+ const result = filterFilesForLlm ( files , ig ( [ "/dist/**" ] ) ) ;
210214 expect ( result . files ) . toHaveLength ( 1 ) ;
211215 expect ( result . files [ 0 ] ?. path ) . toBe ( "src/app.ts" ) ;
212216 } ) ;
213217
214218 it ( "root-anchored pattern does not match nested paths" , ( ) => {
215219 const files = [ makeFile ( { path : "foo/bar.js" } ) , makeFile ( { path : "src/foo/bar.js" } ) ] ;
216- const result = filterFilesForLlm ( files , [ "/foo/**" ] ) ;
220+ const result = filterFilesForLlm ( files , ig ( [ "/foo/**" ] ) ) ;
217221 expect ( result . files ) . toHaveLength ( 1 ) ;
218222 expect ( result . files [ 0 ] ?. path ) . toBe ( "src/foo/bar.js" ) ;
219223 } ) ;
220224
221225 it ( "trailing slash matches directory contents" , ( ) => {
222226 const files = [ makeFile ( { path : "build/output.js" } ) , makeFile ( { path : "src/app.ts" } ) ] ;
223- const result = filterFilesForLlm ( files , [ "build/" ] ) ;
227+ const result = filterFilesForLlm ( files , ig ( [ "build/" ] ) ) ;
224228 expect ( result . files ) . toHaveLength ( 1 ) ;
225229 expect ( result . files [ 0 ] ?. path ) . toBe ( "src/app.ts" ) ;
226230 } ) ;
@@ -230,26 +234,30 @@ describe("filterFilesForLlm", () => {
230234 makeFile ( { path : "generated/schema.ts" } ) ,
231235 makeFile ( { path : "generated/keep-this.ts" } ) ,
232236 ] ;
233- const result = filterFilesForLlm ( files , [ "generated/**" , "!keep-this.ts" ] ) ;
237+ const result = filterFilesForLlm ( files , ig ( [ "generated/**" , "!keep-this.ts" ] ) ) ;
234238 expect ( result . files ) . toHaveLength ( 1 ) ;
235239 expect ( result . files [ 0 ] ?. path ) . toBe ( "generated/keep-this.ts" ) ;
236240 } ) ;
237241} ) ;
238242
239- describe ( "loadStageIgnorePatterns " , ( ) => {
243+ describe ( "loadStageIgnore " , ( ) => {
240244 function makeTempDir ( ) : string {
241245 return mkdtempSync ( path . join ( tmpdir ( ) , "stage-test-" ) ) ;
242246 }
243247
244- it ( "returns empty array when .stageignore does not exist" , ( ) => {
248+ it ( "returns null when .stageignore does not exist" , ( ) => {
245249 const dir = makeTempDir ( ) ;
246- expect ( loadStageIgnorePatterns ( dir ) ) . toEqual ( [ ] ) ;
250+ expect ( loadStageIgnore ( dir ) ) . toBeNull ( ) ;
247251 } ) ;
248252
249253 it ( "parses patterns from .stageignore" , ( ) => {
250254 const dir = makeTempDir ( ) ;
251255 writeFileSync ( path . join ( dir , ".stageignore" ) , "build/**\ndist/**\n" ) ;
252- expect ( loadStageIgnorePatterns ( dir ) ) . toEqual ( [ "build/**" , "dist/**" ] ) ;
256+ const matcher = loadStageIgnore ( dir ) ;
257+ expect ( matcher ) . not . toBeNull ( ) ;
258+ expect ( matcher ?. ignores ( "build/config.gypi" ) ) . toBe ( true ) ;
259+ expect ( matcher ?. ignores ( "dist/bundle.js" ) ) . toBe ( true ) ;
260+ expect ( matcher ?. ignores ( "src/app.ts" ) ) . toBe ( false ) ;
253261 } ) ;
254262
255263 it ( "ignores comments and blank lines" , ( ) => {
@@ -258,12 +266,18 @@ describe("loadStageIgnorePatterns", () => {
258266 path . join ( dir , ".stageignore" ) ,
259267 "# Build artifacts\nbuild/**\n\n# Output\ndist/**\n\n" ,
260268 ) ;
261- expect ( loadStageIgnorePatterns ( dir ) ) . toEqual ( [ "build/**" , "dist/**" ] ) ;
269+ const matcher = loadStageIgnore ( dir ) ;
270+ expect ( matcher ?. ignores ( "build/config.gypi" ) ) . toBe ( true ) ;
271+ expect ( matcher ?. ignores ( "dist/bundle.js" ) ) . toBe ( true ) ;
272+ expect ( matcher ?. ignores ( "src/app.ts" ) ) . toBe ( false ) ;
262273 } ) ;
263274
264- it ( "trims whitespace from patterns " , ( ) => {
275+ it ( "empty .stageignore matches nothing " , ( ) => {
265276 const dir = makeTempDir ( ) ;
266- writeFileSync ( path . join ( dir , ".stageignore" ) , " build/** \n dist/** \n" ) ;
267- expect ( loadStageIgnorePatterns ( dir ) ) . toEqual ( [ "build/**" , "dist/**" ] ) ;
277+ writeFileSync ( path . join ( dir , ".stageignore" ) , "" ) ;
278+ const matcher = loadStageIgnore ( dir ) ;
279+ expect ( matcher ) . not . toBeNull ( ) ;
280+ expect ( matcher ?. ignores ( "src/app.ts" ) ) . toBe ( false ) ;
281+ expect ( matcher ?. ignores ( "build/anything.js" ) ) . toBe ( false ) ;
268282 } ) ;
269283} ) ;
0 commit comments