@@ -178,4 +178,103 @@ describe('config.include / config.exclude (issue #981)', () => {
178178 // Paths are already relative to each run's own tmpDir so they compare directly.
179179 expect ( nativeFiles ) . toEqual ( wasmFiles ) ;
180180 } ) ;
181+
182+ // ── opts.exclude (programmatic, no on-disk config) ───────────────
183+
184+ async function buildWithOptsExclude (
185+ root : string ,
186+ engine : EngineName ,
187+ optsExclude : string [ ] ,
188+ ) : Promise < string [ ] > {
189+ clearConfigCache ( ) ;
190+ const dbDir = path . join ( root , '.codegraph' ) ;
191+ if ( fs . existsSync ( dbDir ) ) fs . rmSync ( dbDir , { recursive : true , force : true } ) ;
192+ await buildGraph ( root , { engine, exclude : optsExclude , skipRegistry : true } ) ;
193+ const files = readFileRows ( path . join ( dbDir , 'graph.db' ) ) ;
194+ return files . map ( ( f ) => f . replace ( / \\ / g, '/' ) ) . sort ( ) ;
195+ }
196+
197+ it ( 'wasm: opts.exclude rejects matching files without writing config' , async ( ) => {
198+ const root = fs . mkdtempSync ( path . join ( tmpDir , 'opts-wasm-' ) ) ;
199+ writeFixture ( root ) ;
200+ const files = await buildWithOptsExclude ( root , 'wasm' , [ '**/*.test.js' , '**/*.spec.js' ] ) ;
201+ expect ( files ) . toContain ( 'src/math.js' ) ;
202+ expect ( files ) . not . toContain ( 'src/math.test.js' ) ;
203+ expect ( files ) . not . toContain ( 'src/util.spec.js' ) ;
204+ } ) ;
205+
206+ itNative ( 'native: opts.exclude rejects matching files without writing config' , async ( ) => {
207+ const root = fs . mkdtempSync ( path . join ( tmpDir , 'opts-native-' ) ) ;
208+ writeFixture ( root ) ;
209+ const files = await buildWithOptsExclude ( root , 'native' , [ '**/*.test.js' , '**/*.spec.js' ] ) ;
210+ expect ( files ) . toContain ( 'src/math.js' ) ;
211+ expect ( files ) . not . toContain ( 'src/math.test.js' ) ;
212+ expect ( files ) . not . toContain ( 'src/util.spec.js' ) ;
213+ } ) ;
214+
215+ // ── opts.exclude incremental round trip ──────────────────────────
216+ //
217+ // Greptile feedback on PR #1134: the opts.exclude tests above always wipe
218+ // the DB before building, so they only exercise the fresh-build path. The
219+ // scenario where files that were previously indexed become excluded on a
220+ // subsequent incremental run (i.e. opts.exclude changes between builds
221+ // against the same DB) was untested. This round trip locks in the
222+ // collect → detect behaviour: the second build must observe the newly
223+ // excluded files as removals and drop them from file_hashes.
224+
225+ async function buildSameDb (
226+ root : string ,
227+ engine : EngineName ,
228+ optsExclude : string [ ] | undefined ,
229+ ) : Promise < string [ ] > {
230+ clearConfigCache ( ) ;
231+ await buildGraph ( root , {
232+ engine,
233+ ...( optsExclude !== undefined ? { exclude : optsExclude } : { } ) ,
234+ skipRegistry : true ,
235+ } ) ;
236+ const files = readFileRows ( path . join ( root , '.codegraph' , 'graph.db' ) ) ;
237+ return files . map ( ( f ) => f . replace ( / \\ / g, '/' ) ) . sort ( ) ;
238+ }
239+
240+ it ( 'wasm: opts.exclude introduced on second incremental build drops previously-indexed files' , async ( ) => {
241+ const root = fs . mkdtempSync ( path . join ( tmpDir , 'opts-inc-wasm-' ) ) ;
242+ writeFixture ( root ) ;
243+ // Wipe DB so the first build is a clean baseline that indexes everything.
244+ const dbDir = path . join ( root , '.codegraph' ) ;
245+ if ( fs . existsSync ( dbDir ) ) fs . rmSync ( dbDir , { recursive : true , force : true } ) ;
246+
247+ // First build: no exclude — every supported file is indexed.
248+ const firstFiles = await buildSameDb ( root , 'wasm' , undefined ) ;
249+ expect ( firstFiles ) . toContain ( 'src/math.test.js' ) ;
250+ expect ( firstFiles ) . toContain ( 'src/util.spec.js' ) ;
251+
252+ // Second build against the same DB with exclude — previously-indexed
253+ // test files must be detected as removals and disappear from file_hashes.
254+ const secondFiles = await buildSameDb ( root , 'wasm' , [ '**/*.test.js' , '**/*.spec.js' ] ) ;
255+ expect ( secondFiles ) . toContain ( 'src/math.js' ) ;
256+ expect ( secondFiles ) . toContain ( 'src/util.js' ) ;
257+ expect ( secondFiles ) . not . toContain ( 'src/math.test.js' ) ;
258+ expect ( secondFiles ) . not . toContain ( 'src/util.spec.js' ) ;
259+ } ) ;
260+
261+ itNative (
262+ 'native: opts.exclude introduced on second incremental build drops previously-indexed files' ,
263+ async ( ) => {
264+ const root = fs . mkdtempSync ( path . join ( tmpDir , 'opts-inc-native-' ) ) ;
265+ writeFixture ( root ) ;
266+ const dbDir = path . join ( root , '.codegraph' ) ;
267+ if ( fs . existsSync ( dbDir ) ) fs . rmSync ( dbDir , { recursive : true , force : true } ) ;
268+
269+ const firstFiles = await buildSameDb ( root , 'native' , undefined ) ;
270+ expect ( firstFiles ) . toContain ( 'src/math.test.js' ) ;
271+ expect ( firstFiles ) . toContain ( 'src/util.spec.js' ) ;
272+
273+ const secondFiles = await buildSameDb ( root , 'native' , [ '**/*.test.js' , '**/*.spec.js' ] ) ;
274+ expect ( secondFiles ) . toContain ( 'src/math.js' ) ;
275+ expect ( secondFiles ) . toContain ( 'src/util.js' ) ;
276+ expect ( secondFiles ) . not . toContain ( 'src/math.test.js' ) ;
277+ expect ( secondFiles ) . not . toContain ( 'src/util.spec.js' ) ;
278+ } ,
279+ ) ;
181280} ) ;
0 commit comments