- Pre-compile all regex patterns as module-level constants in parser.js and jsonRepair.js - Add format detection caching (lastDetectedFormat, formatCacheHits) to skip redundant checks - Add clearFormatCache() export for cache invalidation - Add comprehensive test suite (33 tests) for parser and jsonRepair modules - Fix regex escaping in dynamic patterns (statRegex, fieldRegex) - Improve toFieldKey() regex specificity
155 lines
6.0 KiB
JavaScript
155 lines
6.0 KiB
JavaScript
/**
|
|
* Parser performance tests
|
|
* Tests regex pre-compilation and format caching optimizations
|
|
*/
|
|
|
|
// Test the pre-compiled regex patterns directly by importing them from parser.js
|
|
// We use dynamic import with mocking to avoid the SillyTavern dependency chain
|
|
|
|
describe('Parser regex pre-compilation', () => {
|
|
// Test that pre-compiled regex patterns produce correct results
|
|
// These patterns are now module-level constants in parser.js
|
|
|
|
test('emoji regex pattern works correctly', () => {
|
|
const EMOJI_RE = /^[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{1F000}-\u{1F02F}\u{1F0A0}-\u{1F0FF}\u{1F100}-\u{1F64F}\u{1F680}-\u{1F6FF}\u{1F910}-\u{1F96B}\u{1F980}-\u{1F9E0}\u{FE00}-\u{FE0F}\u{200D}\u{20E3}]+/u;
|
|
expect('😊 Hello'.match(EMOJI_RE)).toBeTruthy();
|
|
expect('Hello'.match(EMOJI_RE)).toBeNull();
|
|
});
|
|
|
|
test('thinking tag regex works correctly', () => {
|
|
const THINKING_TAG_RE = /<think>[\s\S]*?<\/think>/gi;
|
|
const THINKING_TAG_ALT_RE = /<thinking>[\s\S]*?<\/thinking>/gi;
|
|
|
|
const text1 = '<think>thinking content</think>actual content';
|
|
const text2 = '<thinking>thinking content</thinking>actual content';
|
|
|
|
expect(text1.replace(THINKING_TAG_RE, '')).toBe('actual content');
|
|
expect(text2.replace(THINKING_TAG_ALT_RE, '')).toBe('actual content');
|
|
});
|
|
|
|
test('JSON block regex works correctly', () => {
|
|
const JSON_BLOCK_RE = /```json\s*\n([\s\S]*?)```/g;
|
|
const text = '```json\n{"key": "value"}\n```';
|
|
const matches = [...text.matchAll(JSON_BLOCK_RE)];
|
|
expect(matches.length).toBe(1);
|
|
expect(matches[0][1].trim()).toBe('{"key": "value"}');
|
|
});
|
|
|
|
test('code block regex works correctly', () => {
|
|
const CODE_BLOCK_RE = /```([^`]+)```/g;
|
|
const text = '```Stats\n---\nHealth: 100%\n```';
|
|
const matches = [...text.matchAll(CODE_BLOCK_RE)];
|
|
expect(matches.length).toBe(1);
|
|
expect(matches[0][1].trim()).toBe('Stats\n---\nHealth: 100%');
|
|
});
|
|
|
|
test('XML trackers regex works correctly', () => {
|
|
const XML_TRACKERS_RE = /<trackers>([\s\S]*?)<\/trackers>/i;
|
|
const text = '<trackers>content here</trackers>';
|
|
const match = text.match(XML_TRACKERS_RE);
|
|
expect(match).toBeTruthy();
|
|
expect(match[1]).toBe('content here');
|
|
});
|
|
|
|
test('stats section detection regex works', () => {
|
|
const STATS_SECTION_RE = /Stats\s*\n\s*---/i;
|
|
expect('Stats\n---\nHealth: 100%'.match(STATS_SECTION_RE)).toBeTruthy();
|
|
expect('Info Box\n---\nDate: 2024'.match(STATS_SECTION_RE)).toBeNull();
|
|
});
|
|
|
|
test('info box section detection regex works', () => {
|
|
const INFOBOX_SECTION_RE = /Info Box\s*\n\s*---/i;
|
|
expect('Info Box\n---\nDate: 2024'.match(INFOBOX_SECTION_RE)).toBeTruthy();
|
|
expect('Stats\n---\nHealth: 100%'.match(INFOBOX_SECTION_RE)).toBeNull();
|
|
});
|
|
|
|
test('characters section detection regex works', () => {
|
|
const CHARACTERS_SECTION_RE = /Present Characters\s*\n\s*---/i;
|
|
expect('Present Characters\n---\nAlice'.match(CHARACTERS_SECTION_RE)).toBeTruthy();
|
|
expect('Stats\n---\nHealth: 100%'.match(CHARACTERS_SECTION_RE)).toBeNull();
|
|
});
|
|
|
|
test('RPG attribute regexes work', () => {
|
|
const RPG_STR_RE = /STR:\s*(\d+)/i;
|
|
const RPG_DEX_RE = /DEX:\s*(\d+)/i;
|
|
const RPG_LVL_RE = /LVL:\s*(\d+)/i;
|
|
|
|
expect('STR: 15'.match(RPG_STR_RE)?.[1]).toBe('15');
|
|
expect('DEX: 20'.match(RPG_DEX_RE)?.[1]).toBe('20');
|
|
expect('LVL: 10'.match(RPG_LVL_RE)?.[1]).toBe('10');
|
|
});
|
|
|
|
test('placeholder pattern regex works', () => {
|
|
const PLACEHOLDER_PATTERN_RE = /\[([A-Za-z\s\/]+)\]/g;
|
|
const text = '[Location] some text [Mood Emoji]';
|
|
const matches = [...text.matchAll(PLACEHOLDER_PATTERN_RE)];
|
|
expect(matches.length).toBe(2);
|
|
});
|
|
|
|
test('format marker regex works', () => {
|
|
const FORMAT_MARKER_RE = /FORMAT:\s*/gi;
|
|
expect('FORMAT: some text'.replace(FORMAT_MARKER_RE, '')).toBe('some text');
|
|
});
|
|
});
|
|
|
|
describe('Format cache logic', () => {
|
|
test('format cache hit threshold logic', () => {
|
|
// Simulate the format caching logic from parser.js
|
|
let lastDetectedFormat = null;
|
|
let formatCacheHits = 0;
|
|
const FORMAT_CACHE_HIT_THRESHOLD = 3;
|
|
|
|
// Initially no cache
|
|
expect(lastDetectedFormat).toBeNull();
|
|
expect(formatCacheHits).toBe(0);
|
|
|
|
// First detection
|
|
lastDetectedFormat = 'json';
|
|
formatCacheHits = 1;
|
|
expect(formatCacheHits >= FORMAT_CACHE_HIT_THRESHOLD).toBe(false);
|
|
|
|
// Second hit
|
|
formatCacheHits++;
|
|
expect(formatCacheHits >= FORMAT_CACHE_HIT_THRESHOLD).toBe(false);
|
|
|
|
// Third hit - now cache is trusted
|
|
formatCacheHits++;
|
|
expect(formatCacheHits >= FORMAT_CACHE_HIT_THRESHOLD).toBe(true);
|
|
|
|
// Cache clear
|
|
lastDetectedFormat = null;
|
|
formatCacheHits = 0;
|
|
expect(lastDetectedFormat).toBeNull();
|
|
expect(formatCacheHits).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('Regex performance verification', () => {
|
|
test('pre-compiled regex is faster than re-compiling', () => {
|
|
// Pre-compiled constant (module-level)
|
|
const PRECOMPILED_RE = /```json\s*\n([\s\S]*?)```/g;
|
|
|
|
// Simulate per-call compilation (old behavior)
|
|
const compilePerCall = (text) => {
|
|
const re = /```json\s*\n([\s\S]*?)```/g;
|
|
return [...text.matchAll(re)];
|
|
};
|
|
|
|
// Pre-compiled usage (new behavior)
|
|
const usePrecompiled = (text) => {
|
|
// Reset lastIndex for global regex
|
|
PRECOMPILED_RE.lastIndex = 0;
|
|
return [...text.matchAll(PRECOMPILED_RE)];
|
|
};
|
|
|
|
const testText = '```json\n{"key": "value"}\n```';
|
|
|
|
// Both should produce the same result
|
|
const compiled = compilePerCall(testText);
|
|
const precompiled = usePrecompiled(testText);
|
|
|
|
expect(compiled.length).toBe(precompiled.length);
|
|
expect(compiled[0][1]).toBe(precompiled[0][1]);
|
|
});
|
|
});
|