From 27d8c95e3d9af1eeb356d1bfe271642ce2c45cae Mon Sep 17 00:00:00 2001 From: Nigel Bazzeghin Date: Sat, 14 Feb 2026 10:55:31 -0600 Subject: [PATCH] fix: treat touchSet trailing slash as directory prefix glob Closes #72 --- src/lib/merge-train.ts | 4 +++- tests/lib/merge-train.test.ts | 13 +++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/lib/merge-train.ts b/src/lib/merge-train.ts index 42c7e0d..e193280 100644 --- a/src/lib/merge-train.ts +++ b/src/lib/merge-train.ts @@ -73,7 +73,9 @@ export async function validateTouchSet( const violations: string[] = []; for (const file of changedFiles) { const matchesAny = touchSet.some(pattern => { - const glob = new Bun.Glob(pattern); + // Trailing slash means "this directory and everything inside it" + const normalized = pattern.endsWith('/') ? `${pattern}**` : pattern; + const glob = new Bun.Glob(normalized); return glob.match(file); }); if (!matchesAny) { diff --git a/tests/lib/merge-train.test.ts b/tests/lib/merge-train.test.ts index d59e695..489b1d7 100644 --- a/tests/lib/merge-train.test.ts +++ b/tests/lib/merge-train.test.ts @@ -489,6 +489,19 @@ describe('validateTouchSet', () => { expect(result.changedFiles).toContain('src/lib/deep/nested.ts'); }); + it('should treat trailing slash as directory prefix glob', async () => { + await mustExec(['git', 'checkout', '-b', 'feature-trailing-slash', 'main'], testRepo.repoDir); + mkdirSync(join(testRepo.repoDir, 'ai-docs/gap-analysis'), { recursive: true }); + writeFileSync(join(testRepo.repoDir, 'ai-docs/gap-analysis/AUDIT.md'), 'audit\n'); + await mustExec(['git', 'add', '.'], testRepo.repoDir); + await mustExec(['git', 'commit', '-m', 'add audit file'], testRepo.repoDir); + await mustExec(['git', 'checkout', 'main'], testRepo.repoDir); + + const result = await validateTouchSet('feature-trailing-slash', 'main', ['ai-docs/gap-analysis/'], { cwd: testRepo.repoDir }); + expect(result.valid).toBe(true); + expect(result.changedFiles).toContain('ai-docs/gap-analysis/AUDIT.md'); + }); + it('should return valid when job has no changes', async () => { await mustExec(['git', 'checkout', '-b', 'feature-no-changes', 'main'], testRepo.repoDir); await mustExec(['git', 'commit', '--allow-empty', '-m', 'empty'], testRepo.repoDir);