mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-12 19:57:37 +00:00
972f2b9483
* feat: add stack glob patterns and route severity levels Operators can filter notification routes and mute rules with anchored * globs, and routes can target info, warning, or error. Matching is fail-closed for unsafe stored patterns; write paths keep partial-PUT semantics and ReDoS caps. * test: split mute and routing chip tests to avoid dialog race * fix: move stack pattern client validator out of PatternChips * fix: bound stack glob matching and atomic pattern chip saves
76 lines
3.2 KiB
TypeScript
76 lines
3.2 KiB
TypeScript
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']);
|
|
});
|
|
});
|