import { describe, it, expect } from 'vitest'; import { stackPatternMatches, validateStackPatternForRedos, parseStackPatternsInput, cleanStackPatterns, } from '../helpers/stackPattern'; describe('stackPattern', () => { it('matches exact names without wildcards', () => { expect(stackPatternMatches('prod-api', 'prod-api')).toBe(true); expect(stackPatternMatches('Prod-api', 'prod-api')).toBe(false); }); it('matches anchored case-sensitive globs', () => { expect(stackPatternMatches('prod-api', 'prod-*')).toBe(true); expect(stackPatternMatches('staging-api', 'prod-*')).toBe(false); expect(stackPatternMatches('prod-api-extra', 'prod-*')).toBe(true); expect(stackPatternMatches('xprod-api', 'prod-*')).toBe(false); }); it('treats ? and regex metacharacters as literal', () => { expect(stackPatternMatches('a?b', 'a?b')).toBe(true); expect(stackPatternMatches('ab', 'a?b')).toBe(false); expect(stackPatternMatches('a.b', 'a.b')).toBe(true); expect(stackPatternMatches('axb', 'a.b')).toBe(false); expect(stackPatternMatches('a+b', 'a+b')).toBe(true); }); it('fails closed on write-cap rejected patterns without throwing', () => { expect(() => stackPatternMatches('anything', '****')).not.toThrow(); expect(stackPatternMatches('anything', '****')).toBe(false); expect(stackPatternMatches('x', 'a'.repeat(201))).toBe(false); expect(stackPatternMatches('x', `${'a*'.repeat(9)}`)).toBe(false); }); it('matches accepted separated-star patterns without RegExp backtracking', () => { const pattern = '*a*a*a*a*a*a*a*a'; expect(validateStackPatternForRedos(pattern)).toBeNull(); expect(stackPatternMatches('aaaaaaaa', pattern)).toBe(true); const nonMatch = `${'a'.repeat(80)}b`; const started = performance.now(); expect(stackPatternMatches(nonMatch, pattern)).toBe(false); expect(performance.now() - started).toBeLessThan(50); }); it('OR semantics are call-site: any pattern may match', () => { const patterns = ['staging-*', 'prod-api']; expect(patterns.some((p) => stackPatternMatches('prod-api', p))).toBe(true); expect(patterns.some((p) => stackPatternMatches('staging-web', p))).toBe(true); expect(patterns.some((p) => stackPatternMatches('dev-web', p))).toBe(false); }); it('validateStackPatternForRedos rejects unsafe inputs', () => { expect(validateStackPatternForRedos('ok-*')).toBeNull(); expect(validateStackPatternForRedos('****')).toMatch(/consecutive/); expect(validateStackPatternForRedos('a'.repeat(201))).toMatch(/too long/); expect(validateStackPatternForRedos('*a*a*a*a*a*a*a*a')).toBeNull(); }); it('parseStackPatternsInput rejects non-arrays and non-strings', () => { expect(parseStackPatternsInput(null).ok).toBe(false); expect(parseStackPatternsInput('prod-*').ok).toBe(false); expect(parseStackPatternsInput([1]).ok).toBe(false); expect(parseStackPatternsInput(['prod-*', '****']).ok).toBe(false); expect(parseStackPatternsInput([' prod-* ', 'prod-*'])).toEqual({ ok: true, patterns: ['prod-*'], }); }); it('cleanStackPatterns trims, drops blanks, dedupes', () => { expect(cleanStackPatterns([' a ', '', 'a', 'b'])).toEqual(['a', 'b']); }); });