error-rule-detector.test.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. /**
  2. * Integration Tests for ErrorRuleDetector Cache Refresh and EventEmitter
  3. *
  4. * Purpose:
  5. * - Test manual cache reload functionality
  6. * - Test EventEmitter-driven automatic cache refresh
  7. * - Test cache statistics and state management
  8. * - Test safe-regex ReDoS detection and filtering
  9. * - Test performance of database-driven detection vs hardcoded regex
  10. *
  11. * Test Coverage:
  12. * 1. Manual reload() method
  13. * 2. EventEmitter 'errorRulesUpdated' event handling
  14. * 3. Cache statistics (getStats, isEmpty)
  15. * 4. ReDoS risk detection with safe-regex
  16. * 5. Performance benchmarking
  17. */
  18. import { beforeAll, describe, expect, test } from "vitest";
  19. import { errorRuleDetector } from "@/lib/error-rule-detector";
  20. import { eventEmitter } from "@/lib/event-emitter";
  21. // Wait for initial cache load
  22. beforeAll(async () => {
  23. await new Promise((resolve) => setTimeout(resolve, 1000));
  24. });
  25. describe("ErrorRuleDetector Manual Reload", () => {
  26. test("should reload cache successfully", async () => {
  27. const statsBefore = errorRuleDetector.getStats();
  28. await errorRuleDetector.reload();
  29. const statsAfter = errorRuleDetector.getStats();
  30. // Verify cache was reloaded
  31. expect(statsAfter.lastReloadTime).toBeGreaterThanOrEqual(statsBefore.lastReloadTime);
  32. expect(statsAfter.isLoading).toBe(false);
  33. expect(statsAfter.totalCount).toBeGreaterThanOrEqual(7); // At least 7 default rules
  34. });
  35. test("should not allow concurrent reloads", async () => {
  36. // Trigger multiple reloads simultaneously
  37. const promises = [
  38. errorRuleDetector.reload(),
  39. errorRuleDetector.reload(),
  40. errorRuleDetector.reload(),
  41. ];
  42. await Promise.all(promises);
  43. // Should complete without errors
  44. const stats = errorRuleDetector.getStats();
  45. expect(stats.isLoading).toBe(false);
  46. });
  47. test("should update lastReloadTime on reload", async () => {
  48. const before = errorRuleDetector.getStats().lastReloadTime;
  49. await new Promise((resolve) => setTimeout(resolve, 10)); // Ensure time difference
  50. await errorRuleDetector.reload();
  51. const after = errorRuleDetector.getStats().lastReloadTime;
  52. expect(after).toBeGreaterThan(before);
  53. });
  54. });
  55. describe("EventEmitter Integration", () => {
  56. test("should auto-reload on 'errorRulesUpdated' event", async () => {
  57. const statsBefore = errorRuleDetector.getStats();
  58. // Emit event to trigger auto-reload
  59. eventEmitter.emit("errorRulesUpdated");
  60. // Wait for async reload to complete
  61. await new Promise((resolve) => setTimeout(resolve, 200));
  62. const statsAfter = errorRuleDetector.getStats();
  63. // Verify cache was refreshed
  64. expect(statsAfter.lastReloadTime).toBeGreaterThanOrEqual(statsBefore.lastReloadTime);
  65. });
  66. test("should handle multiple event emissions gracefully", async () => {
  67. // Emit multiple events in quick succession
  68. for (let i = 0; i < 5; i++) {
  69. eventEmitter.emit("errorRulesUpdated");
  70. }
  71. // Wait for all reloads to complete
  72. await new Promise((resolve) => setTimeout(resolve, 500));
  73. const stats = errorRuleDetector.getStats();
  74. expect(stats.isLoading).toBe(false);
  75. expect(stats.totalCount).toBeGreaterThanOrEqual(7);
  76. });
  77. });
  78. describe("Cache Statistics and State", () => {
  79. test("should return correct statistics", () => {
  80. const stats = errorRuleDetector.getStats();
  81. // Verify structure
  82. expect(stats).toHaveProperty("regexCount");
  83. expect(stats).toHaveProperty("containsCount");
  84. expect(stats).toHaveProperty("exactCount");
  85. expect(stats).toHaveProperty("totalCount");
  86. expect(stats).toHaveProperty("lastReloadTime");
  87. expect(stats).toHaveProperty("isLoading");
  88. // Verify values
  89. expect(typeof stats.regexCount).toBe("number");
  90. expect(typeof stats.containsCount).toBe("number");
  91. expect(typeof stats.exactCount).toBe("number");
  92. expect(stats.totalCount).toBe(stats.regexCount + stats.containsCount + stats.exactCount);
  93. expect(stats.totalCount).toBeGreaterThanOrEqual(7); // At least 7 default rules
  94. });
  95. test("should not be empty after initialization", () => {
  96. expect(errorRuleDetector.isEmpty()).toBe(false);
  97. });
  98. test("should have valid lastReloadTime", () => {
  99. const stats = errorRuleDetector.getStats();
  100. const now = Date.now();
  101. expect(stats.lastReloadTime).toBeGreaterThan(0);
  102. expect(stats.lastReloadTime).toBeLessThanOrEqual(now);
  103. });
  104. });
  105. describe("Error Detection Functionality", () => {
  106. test("should detect matching error", () => {
  107. const result = errorRuleDetector.detect("prompt is too long: 5000 tokens > 4096 maximum");
  108. expect(result.matched).toBe(true);
  109. expect(result.category).toBeTruthy();
  110. expect(result.pattern).toBeTruthy();
  111. expect(result.matchType).toMatch(/regex|contains|exact/);
  112. });
  113. test("should return detailed match information", () => {
  114. const result = errorRuleDetector.detect("blocked by our content filter policy");
  115. expect(result.matched).toBe(true);
  116. expect(result.category).toBe("content_filter");
  117. expect(result.pattern).toBeTruthy();
  118. expect(result.matchType).toBeTruthy();
  119. });
  120. test("should not match unrelated error", () => {
  121. const result = errorRuleDetector.detect("Network timeout error");
  122. expect(result.matched).toBe(false);
  123. expect(result.category).toBeUndefined();
  124. expect(result.pattern).toBeUndefined();
  125. expect(result.matchType).toBeUndefined();
  126. });
  127. test("should handle empty string", () => {
  128. const result = errorRuleDetector.detect("");
  129. expect(result.matched).toBe(false);
  130. });
  131. test("should be case-insensitive", () => {
  132. const result1 = errorRuleDetector.detect("PROMPT IS TOO LONG: 5000 TOKENS > 4096 MAXIMUM");
  133. const result2 = errorRuleDetector.detect("Prompt Is Too Long: 5000 Tokens > 4096 Maximum");
  134. expect(result1.matched).toBe(true);
  135. expect(result2.matched).toBe(true);
  136. });
  137. });
  138. describe("Performance Testing", () => {
  139. test("should detect errors efficiently", () => {
  140. const testMessages = [
  141. "prompt is too long: 5000 tokens > 4096 maximum",
  142. "blocked by our content filter policy",
  143. "PDF has too many pages: 150 > 100 maximum pages",
  144. "Network timeout",
  145. "Internal server error",
  146. ];
  147. const start = performance.now();
  148. for (let i = 0; i < 1000; i++) {
  149. for (const msg of testMessages) {
  150. errorRuleDetector.detect(msg);
  151. }
  152. }
  153. const end = performance.now();
  154. const duration = end - start;
  155. // Should complete 5000 detections in under 100ms
  156. console.log(`Performance: 5000 detections in ${duration.toFixed(2)}ms`);
  157. expect(duration).toBeLessThan(100);
  158. });
  159. test("should cache regex compilation", () => {
  160. const message = "prompt is too long: 5000 tokens > 4096 maximum";
  161. // First detection (might compile regex)
  162. const start1 = performance.now();
  163. errorRuleDetector.detect(message);
  164. const duration1 = performance.now() - start1;
  165. // Subsequent detections (uses cached regex)
  166. const start2 = performance.now();
  167. for (let i = 0; i < 100; i++) {
  168. errorRuleDetector.detect(message);
  169. }
  170. const duration2 = performance.now() - start2;
  171. console.log(
  172. `First detection: ${duration1.toFixed(2)}ms, 100 cached: ${duration2.toFixed(2)}ms`
  173. );
  174. // Cached detections should be fast
  175. expect(duration2).toBeLessThan(10);
  176. });
  177. });
  178. describe("Safe-Regex ReDoS Detection", () => {
  179. /**
  180. * Note: These tests verify that ErrorRuleDetector skips dangerous regex patterns.
  181. * The actual ReDoS validation happens in the Server Action layer (error-rules.ts),
  182. * but ErrorRuleDetector also filters them during cache loading.
  183. */
  184. test("should skip loading dangerous regex patterns", async () => {
  185. const _statsBefore = errorRuleDetector.getStats();
  186. // Reload cache (which should skip any dangerous patterns)
  187. await errorRuleDetector.reload();
  188. const statsAfter = errorRuleDetector.getStats();
  189. // All loaded regex patterns should be safe
  190. // (If there were dangerous patterns in DB, they would be skipped and logged)
  191. expect(statsAfter.totalCount).toBeGreaterThanOrEqual(7);
  192. });
  193. test("should log warning for ReDoS patterns", async () => {
  194. // This test verifies the behavior when dangerous patterns exist in DB
  195. // In practice, such patterns should be blocked by Server Action validation
  196. // but ErrorRuleDetector provides defense-in-depth
  197. const consoleLogs: string[] = [];
  198. const originalWarn = console.warn;
  199. console.warn = (...args: unknown[]) => {
  200. consoleLogs.push(args.join(" "));
  201. originalWarn(...args);
  202. };
  203. await errorRuleDetector.reload();
  204. console.warn = originalWarn;
  205. // If there were any ReDoS patterns, they should be logged
  206. // (In a clean database, this should be empty)
  207. const redosWarnings = consoleLogs.filter((log) => log.includes("ReDoS"));
  208. console.log(`ReDoS warnings found: ${redosWarnings.length}`);
  209. });
  210. });
  211. describe("Match Type Priority", () => {
  212. /**
  213. * Test that detection order follows performance optimization:
  214. * 1. Contains matching (fastest)
  215. * 2. Exact matching (O(1) lookup)
  216. * 3. Regex matching (slowest but most flexible)
  217. */
  218. test("should return match type information", () => {
  219. const testMessages = [
  220. "prompt is too long: 5000 tokens > 4096 maximum",
  221. "blocked by our content filter",
  222. ];
  223. for (const message of testMessages) {
  224. const result = errorRuleDetector.detect(message);
  225. expect(result.matched).toBe(true);
  226. expect(result.matchType).toBeTruthy();
  227. // Match type should be one of the valid types
  228. if (result.matchType) {
  229. expect(["regex", "contains", "exact"]).toContain(result.matchType);
  230. }
  231. }
  232. });
  233. });
  234. describe("Cache Failure Handling", () => {
  235. test("should handle database errors gracefully", async () => {
  236. // ErrorRuleDetector should not throw on database errors
  237. // It should log errors and keep existing cache (fail-safe design)
  238. await expect(errorRuleDetector.reload()).resolves.toBeUndefined();
  239. });
  240. test("should maintain existing cache on reload failure", async () => {
  241. const statsBefore = errorRuleDetector.getStats();
  242. // Even if reload fails, cache should remain usable
  243. await errorRuleDetector.reload();
  244. const statsAfter = errorRuleDetector.getStats();
  245. // Cache should still be functional
  246. expect(statsAfter.totalCount).toBeGreaterThanOrEqual(statsBefore.totalCount);
  247. });
  248. });