request-filter-advanced.test.ts 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985
  1. import { beforeEach, describe, expect, test } from "vitest";
  2. import { requestFilterEngine } from "@/lib/request-filter-engine";
  3. import type { RequestFilter } from "@/repository/request-filters";
  4. import type { FilterOperation } from "@/lib/request-filter-types";
  5. // =============================================================================
  6. // Helpers
  7. // =============================================================================
  8. let filterId = 0;
  9. function createFilter(overrides: Partial<RequestFilter>): RequestFilter {
  10. return {
  11. id: ++filterId,
  12. name: `adv-filter-${filterId}`,
  13. description: null,
  14. scope: "body",
  15. action: "json_path",
  16. matchType: null,
  17. target: "",
  18. replacement: null,
  19. priority: 0,
  20. isEnabled: true,
  21. bindingType: "global",
  22. providerIds: null,
  23. groupTags: null,
  24. ruleMode: "simple",
  25. executionPhase: "guard",
  26. operations: null,
  27. createdAt: new Date(),
  28. updatedAt: new Date(),
  29. ...overrides,
  30. };
  31. }
  32. function createAdvancedFilter(
  33. operations: FilterOperation[],
  34. overrides: Partial<RequestFilter> = {}
  35. ): RequestFilter {
  36. return createFilter({
  37. ruleMode: "advanced",
  38. executionPhase: "final",
  39. operations,
  40. ...overrides,
  41. });
  42. }
  43. // =============================================================================
  44. // Insert Operations
  45. // =============================================================================
  46. describe("Advanced Mode - Insert Operations", () => {
  47. beforeEach(() => {
  48. filterId = 0;
  49. requestFilterEngine.setFiltersForTest([]);
  50. });
  51. test("insert at end (default): appends to array", async () => {
  52. const body: Record<string, unknown> = {
  53. messages: [{ role: "user", content: "hello" }],
  54. };
  55. const headers = new Headers();
  56. const filter = createAdvancedFilter([
  57. {
  58. op: "insert",
  59. scope: "body",
  60. path: "messages",
  61. value: { role: "system", content: "You are helpful" },
  62. },
  63. ]);
  64. requestFilterEngine.setFiltersForTest([filter]);
  65. await requestFilterEngine.applyFinal(
  66. { provider: { id: 1, groupTag: null } } as Parameters<
  67. typeof requestFilterEngine.applyFinal
  68. >[0],
  69. body,
  70. headers
  71. );
  72. const msgs = body.messages as Array<Record<string, string>>;
  73. expect(msgs).toHaveLength(2);
  74. expect(msgs[1]).toEqual({ role: "system", content: "You are helpful" });
  75. });
  76. test("insert at start: prepends to array", async () => {
  77. const body: Record<string, unknown> = {
  78. messages: [{ role: "user", content: "hello" }],
  79. };
  80. const headers = new Headers();
  81. const filter = createAdvancedFilter([
  82. {
  83. op: "insert",
  84. scope: "body",
  85. path: "messages",
  86. value: { role: "system", content: "system prompt" },
  87. position: "start",
  88. },
  89. ]);
  90. requestFilterEngine.setFiltersForTest([filter]);
  91. await requestFilterEngine.applyFinal(
  92. { provider: { id: 1, groupTag: null } } as Parameters<
  93. typeof requestFilterEngine.applyFinal
  94. >[0],
  95. body,
  96. headers
  97. );
  98. const msgs = body.messages as Array<Record<string, string>>;
  99. expect(msgs).toHaveLength(2);
  100. expect(msgs[0]).toEqual({ role: "system", content: "system prompt" });
  101. });
  102. test("insert before anchor: anchor found -> correct position", async () => {
  103. const body: Record<string, unknown> = {
  104. messages: [
  105. { role: "system", content: "existing" },
  106. { role: "user", content: "hello" },
  107. ],
  108. };
  109. const headers = new Headers();
  110. const filter = createAdvancedFilter([
  111. {
  112. op: "insert",
  113. scope: "body",
  114. path: "messages",
  115. value: { role: "system", content: "injected" },
  116. position: "before",
  117. anchor: { field: "role", value: "user" },
  118. },
  119. ]);
  120. requestFilterEngine.setFiltersForTest([filter]);
  121. await requestFilterEngine.applyFinal(
  122. { provider: { id: 1, groupTag: null } } as Parameters<
  123. typeof requestFilterEngine.applyFinal
  124. >[0],
  125. body,
  126. headers
  127. );
  128. const msgs = body.messages as Array<Record<string, string>>;
  129. expect(msgs).toHaveLength(3);
  130. expect(msgs[1]).toEqual({ role: "system", content: "injected" });
  131. expect(msgs[2]).toEqual({ role: "user", content: "hello" });
  132. });
  133. test("insert after anchor: anchor found -> correct position", async () => {
  134. const body: Record<string, unknown> = {
  135. messages: [
  136. { role: "system", content: "sys" },
  137. { role: "user", content: "hello" },
  138. { role: "assistant", content: "hi" },
  139. ],
  140. };
  141. const headers = new Headers();
  142. const filter = createAdvancedFilter([
  143. {
  144. op: "insert",
  145. scope: "body",
  146. path: "messages",
  147. value: { role: "system", content: "after-user" },
  148. position: "after",
  149. anchor: { field: "role", value: "user" },
  150. },
  151. ]);
  152. requestFilterEngine.setFiltersForTest([filter]);
  153. await requestFilterEngine.applyFinal(
  154. { provider: { id: 1, groupTag: null } } as Parameters<
  155. typeof requestFilterEngine.applyFinal
  156. >[0],
  157. body,
  158. headers
  159. );
  160. const msgs = body.messages as Array<Record<string, string>>;
  161. expect(msgs).toHaveLength(4);
  162. expect(msgs[2]).toEqual({ role: "system", content: "after-user" });
  163. });
  164. test("onAnchorMissing: start - anchor not found -> prepend", async () => {
  165. const body: Record<string, unknown> = {
  166. messages: [{ role: "user", content: "hello" }],
  167. };
  168. const headers = new Headers();
  169. const filter = createAdvancedFilter([
  170. {
  171. op: "insert",
  172. scope: "body",
  173. path: "messages",
  174. value: { role: "system", content: "fallback-start" },
  175. position: "before",
  176. anchor: { field: "role", value: "nonexistent" },
  177. onAnchorMissing: "start",
  178. },
  179. ]);
  180. requestFilterEngine.setFiltersForTest([filter]);
  181. await requestFilterEngine.applyFinal(
  182. { provider: { id: 1, groupTag: null } } as Parameters<
  183. typeof requestFilterEngine.applyFinal
  184. >[0],
  185. body,
  186. headers
  187. );
  188. const msgs = body.messages as Array<Record<string, string>>;
  189. expect(msgs).toHaveLength(2);
  190. expect(msgs[0]).toEqual({ role: "system", content: "fallback-start" });
  191. });
  192. test("onAnchorMissing: end - anchor not found -> append", async () => {
  193. const body: Record<string, unknown> = {
  194. messages: [{ role: "user", content: "hello" }],
  195. };
  196. const headers = new Headers();
  197. const filter = createAdvancedFilter([
  198. {
  199. op: "insert",
  200. scope: "body",
  201. path: "messages",
  202. value: { role: "system", content: "fallback-end" },
  203. position: "after",
  204. anchor: { field: "role", value: "nonexistent" },
  205. onAnchorMissing: "end",
  206. },
  207. ]);
  208. requestFilterEngine.setFiltersForTest([filter]);
  209. await requestFilterEngine.applyFinal(
  210. { provider: { id: 1, groupTag: null } } as Parameters<
  211. typeof requestFilterEngine.applyFinal
  212. >[0],
  213. body,
  214. headers
  215. );
  216. const msgs = body.messages as Array<Record<string, string>>;
  217. expect(msgs).toHaveLength(2);
  218. expect(msgs[1]).toEqual({ role: "system", content: "fallback-end" });
  219. });
  220. test("onAnchorMissing: skip - anchor not found -> no insertion", async () => {
  221. const body: Record<string, unknown> = {
  222. messages: [{ role: "user", content: "hello" }],
  223. };
  224. const headers = new Headers();
  225. const filter = createAdvancedFilter([
  226. {
  227. op: "insert",
  228. scope: "body",
  229. path: "messages",
  230. value: { role: "system", content: "skipped" },
  231. position: "before",
  232. anchor: { field: "role", value: "nonexistent" },
  233. onAnchorMissing: "skip",
  234. },
  235. ]);
  236. requestFilterEngine.setFiltersForTest([filter]);
  237. await requestFilterEngine.applyFinal(
  238. { provider: { id: 1, groupTag: null } } as Parameters<
  239. typeof requestFilterEngine.applyFinal
  240. >[0],
  241. body,
  242. headers
  243. );
  244. const msgs = body.messages as Array<Record<string, string>>;
  245. expect(msgs).toHaveLength(1);
  246. });
  247. test("dedupe (deep equal): exact duplicate exists -> skip", async () => {
  248. const body: Record<string, unknown> = {
  249. messages: [
  250. { role: "system", content: "You are helpful" },
  251. { role: "user", content: "hello" },
  252. ],
  253. };
  254. const headers = new Headers();
  255. const filter = createAdvancedFilter([
  256. {
  257. op: "insert",
  258. scope: "body",
  259. path: "messages",
  260. value: { role: "system", content: "You are helpful" },
  261. position: "start",
  262. },
  263. ]);
  264. requestFilterEngine.setFiltersForTest([filter]);
  265. await requestFilterEngine.applyFinal(
  266. { provider: { id: 1, groupTag: null } } as Parameters<
  267. typeof requestFilterEngine.applyFinal
  268. >[0],
  269. body,
  270. headers
  271. );
  272. const msgs = body.messages as Array<Record<string, string>>;
  273. expect(msgs).toHaveLength(2); // no insertion, duplicate found
  274. });
  275. test("dedupe (byFields): partial field match exists -> skip", async () => {
  276. const body: Record<string, unknown> = {
  277. messages: [
  278. { role: "system", content: "old system prompt" },
  279. { role: "user", content: "hello" },
  280. ],
  281. };
  282. const headers = new Headers();
  283. const filter = createAdvancedFilter([
  284. {
  285. op: "insert",
  286. scope: "body",
  287. path: "messages",
  288. value: { role: "system", content: "new system prompt" },
  289. position: "start",
  290. dedupe: { byFields: ["role"] },
  291. },
  292. ]);
  293. requestFilterEngine.setFiltersForTest([filter]);
  294. await requestFilterEngine.applyFinal(
  295. { provider: { id: 1, groupTag: null } } as Parameters<
  296. typeof requestFilterEngine.applyFinal
  297. >[0],
  298. body,
  299. headers
  300. );
  301. const msgs = body.messages as Array<Record<string, string>>;
  302. expect(msgs).toHaveLength(2); // skipped because role:"system" already exists
  303. });
  304. test("dedupe disabled: duplicate exists -> insert anyway", async () => {
  305. const body: Record<string, unknown> = {
  306. messages: [
  307. { role: "system", content: "You are helpful" },
  308. { role: "user", content: "hello" },
  309. ],
  310. };
  311. const headers = new Headers();
  312. const filter = createAdvancedFilter([
  313. {
  314. op: "insert",
  315. scope: "body",
  316. path: "messages",
  317. value: { role: "system", content: "You are helpful" },
  318. position: "start",
  319. dedupe: { enabled: false },
  320. },
  321. ]);
  322. requestFilterEngine.setFiltersForTest([filter]);
  323. await requestFilterEngine.applyFinal(
  324. { provider: { id: 1, groupTag: null } } as Parameters<
  325. typeof requestFilterEngine.applyFinal
  326. >[0],
  327. body,
  328. headers
  329. );
  330. const msgs = body.messages as Array<Record<string, string>>;
  331. expect(msgs).toHaveLength(3); // inserted despite duplicate
  332. expect(msgs[0]).toEqual({ role: "system", content: "You are helpful" });
  333. });
  334. test("insert into non-existent array: creates array at path", async () => {
  335. const body: Record<string, unknown> = {};
  336. const headers = new Headers();
  337. const filter = createAdvancedFilter([
  338. {
  339. op: "insert",
  340. scope: "body",
  341. path: "tools",
  342. value: { type: "computer_20241022" },
  343. },
  344. ]);
  345. requestFilterEngine.setFiltersForTest([filter]);
  346. await requestFilterEngine.applyFinal(
  347. { provider: { id: 1, groupTag: null } } as Parameters<
  348. typeof requestFilterEngine.applyFinal
  349. >[0],
  350. body,
  351. headers
  352. );
  353. expect(body.tools).toEqual([{ type: "computer_20241022" }]);
  354. });
  355. });
  356. // =============================================================================
  357. // Remove Operations
  358. // =============================================================================
  359. describe("Advanced Mode - Remove Operations", () => {
  360. beforeEach(() => {
  361. filterId = 0;
  362. requestFilterEngine.setFiltersForTest([]);
  363. });
  364. test("remove body path: deletes nested field", async () => {
  365. const body: Record<string, unknown> = {
  366. metadata: { user_id: "abc", internal: "secret" },
  367. };
  368. const headers = new Headers();
  369. const filter = createAdvancedFilter([
  370. { op: "remove", scope: "body", path: "metadata.internal" },
  371. ]);
  372. requestFilterEngine.setFiltersForTest([filter]);
  373. await requestFilterEngine.applyFinal(
  374. { provider: { id: 1, groupTag: null } } as Parameters<
  375. typeof requestFilterEngine.applyFinal
  376. >[0],
  377. body,
  378. headers
  379. );
  380. const metadata = body.metadata as Record<string, string>;
  381. expect(metadata.user_id).toBe("abc");
  382. expect(metadata.internal).toBeUndefined();
  383. });
  384. test("remove array elements by matcher: removes all matching", async () => {
  385. const body: Record<string, unknown> = {
  386. messages: [
  387. { role: "system", content: "a" },
  388. { role: "user", content: "b" },
  389. { role: "system", content: "c" },
  390. ],
  391. };
  392. const headers = new Headers();
  393. const filter = createAdvancedFilter([
  394. {
  395. op: "remove",
  396. scope: "body",
  397. path: "messages",
  398. matcher: { field: "role", value: "system" },
  399. },
  400. ]);
  401. requestFilterEngine.setFiltersForTest([filter]);
  402. await requestFilterEngine.applyFinal(
  403. { provider: { id: 1, groupTag: null } } as Parameters<
  404. typeof requestFilterEngine.applyFinal
  405. >[0],
  406. body,
  407. headers
  408. );
  409. const msgs = body.messages as Array<Record<string, string>>;
  410. expect(msgs).toHaveLength(1);
  411. expect(msgs[0].role).toBe("user");
  412. });
  413. test("remove header: deletes header", async () => {
  414. const body: Record<string, unknown> = {};
  415. const headers = new Headers({ "x-internal": "secret", "x-keep": "yes" });
  416. const filter = createAdvancedFilter([{ op: "remove", scope: "header", path: "x-internal" }]);
  417. requestFilterEngine.setFiltersForTest([filter]);
  418. await requestFilterEngine.applyFinal(
  419. { provider: { id: 1, groupTag: null } } as Parameters<
  420. typeof requestFilterEngine.applyFinal
  421. >[0],
  422. body,
  423. headers
  424. );
  425. expect(headers.has("x-internal")).toBe(false);
  426. expect(headers.get("x-keep")).toBe("yes");
  427. });
  428. });
  429. // =============================================================================
  430. // Set Operations
  431. // =============================================================================
  432. describe("Advanced Mode - Set Operations", () => {
  433. beforeEach(() => {
  434. filterId = 0;
  435. requestFilterEngine.setFiltersForTest([]);
  436. });
  437. test("set body path (overwrite): overwrites existing value", async () => {
  438. const body: Record<string, unknown> = { model: "old-model" };
  439. const headers = new Headers();
  440. const filter = createAdvancedFilter([
  441. { op: "set", scope: "body", path: "model", value: "new-model" },
  442. ]);
  443. requestFilterEngine.setFiltersForTest([filter]);
  444. await requestFilterEngine.applyFinal(
  445. { provider: { id: 1, groupTag: null } } as Parameters<
  446. typeof requestFilterEngine.applyFinal
  447. >[0],
  448. body,
  449. headers
  450. );
  451. expect(body.model).toBe("new-model");
  452. });
  453. test("set body path (if_missing): skips when exists, sets when missing", async () => {
  454. const body: Record<string, unknown> = { model: "existing" };
  455. const headers = new Headers();
  456. const filter = createAdvancedFilter([
  457. {
  458. op: "set",
  459. scope: "body",
  460. path: "model",
  461. value: "should-not-set",
  462. writeMode: "if_missing",
  463. },
  464. {
  465. op: "set",
  466. scope: "body",
  467. path: "max_tokens",
  468. value: 4096,
  469. writeMode: "if_missing",
  470. },
  471. ]);
  472. requestFilterEngine.setFiltersForTest([filter]);
  473. await requestFilterEngine.applyFinal(
  474. { provider: { id: 1, groupTag: null } } as Parameters<
  475. typeof requestFilterEngine.applyFinal
  476. >[0],
  477. body,
  478. headers
  479. );
  480. expect(body.model).toBe("existing"); // unchanged
  481. expect(body.max_tokens).toBe(4096); // set because missing
  482. });
  483. test("set header (overwrite/if_missing): same logic", async () => {
  484. const body: Record<string, unknown> = {};
  485. const headers = new Headers({ "x-existing": "old" });
  486. const filter = createAdvancedFilter([
  487. { op: "set", scope: "header", path: "x-existing", value: "new" },
  488. {
  489. op: "set",
  490. scope: "header",
  491. path: "x-new",
  492. value: "created",
  493. writeMode: "if_missing",
  494. },
  495. {
  496. op: "set",
  497. scope: "header",
  498. path: "x-existing",
  499. value: "should-not-overwrite",
  500. writeMode: "if_missing",
  501. },
  502. ]);
  503. requestFilterEngine.setFiltersForTest([filter]);
  504. await requestFilterEngine.applyFinal(
  505. { provider: { id: 1, groupTag: null } } as Parameters<
  506. typeof requestFilterEngine.applyFinal
  507. >[0],
  508. body,
  509. headers
  510. );
  511. expect(headers.get("x-existing")).toBe("new"); // overwritten by first op, if_missing skipped
  512. expect(headers.get("x-new")).toBe("created");
  513. });
  514. test("set creates intermediate objects when path doesn't exist", async () => {
  515. const body: Record<string, unknown> = {};
  516. const headers = new Headers();
  517. const filter = createAdvancedFilter([
  518. { op: "set", scope: "body", path: "metadata.user_id", value: "u123" },
  519. ]);
  520. requestFilterEngine.setFiltersForTest([filter]);
  521. await requestFilterEngine.applyFinal(
  522. { provider: { id: 1, groupTag: null } } as Parameters<
  523. typeof requestFilterEngine.applyFinal
  524. >[0],
  525. body,
  526. headers
  527. );
  528. expect((body.metadata as Record<string, string>).user_id).toBe("u123");
  529. });
  530. });
  531. // =============================================================================
  532. // Merge Operations
  533. // =============================================================================
  534. describe("Advanced Mode - Merge Operations", () => {
  535. beforeEach(() => {
  536. filterId = 0;
  537. requestFilterEngine.setFiltersForTest([]);
  538. });
  539. test("deep merge adds new fields", async () => {
  540. const body: Record<string, unknown> = {
  541. metadata: { user_id: "abc" },
  542. };
  543. const headers = new Headers();
  544. const filter = createAdvancedFilter([
  545. {
  546. op: "merge",
  547. scope: "body",
  548. path: "metadata",
  549. value: { session_id: "s123", tag: "test" },
  550. },
  551. ]);
  552. requestFilterEngine.setFiltersForTest([filter]);
  553. await requestFilterEngine.applyFinal(
  554. { provider: { id: 1, groupTag: null } } as Parameters<
  555. typeof requestFilterEngine.applyFinal
  556. >[0],
  557. body,
  558. headers
  559. );
  560. const metadata = body.metadata as Record<string, string>;
  561. expect(metadata.user_id).toBe("abc");
  562. expect(metadata.session_id).toBe("s123");
  563. expect(metadata.tag).toBe("test");
  564. });
  565. test("deep merge overwrites existing fields", async () => {
  566. const body: Record<string, unknown> = {
  567. config: { temperature: 0.7, model: "old" },
  568. };
  569. const headers = new Headers();
  570. const filter = createAdvancedFilter([
  571. {
  572. op: "merge",
  573. scope: "body",
  574. path: "config",
  575. value: { model: "new" },
  576. },
  577. ]);
  578. requestFilterEngine.setFiltersForTest([filter]);
  579. await requestFilterEngine.applyFinal(
  580. { provider: { id: 1, groupTag: null } } as Parameters<
  581. typeof requestFilterEngine.applyFinal
  582. >[0],
  583. body,
  584. headers
  585. );
  586. const config = body.config as Record<string, unknown>;
  587. expect(config.model).toBe("new");
  588. expect(config.temperature).toBe(0.7);
  589. });
  590. test("deep merge with null value deletes field", async () => {
  591. const body: Record<string, unknown> = {
  592. metadata: { user_id: "abc", internal_tracking: "xyz" },
  593. };
  594. const headers = new Headers();
  595. const filter = createAdvancedFilter([
  596. {
  597. op: "merge",
  598. scope: "body",
  599. path: "metadata",
  600. value: { internal_tracking: null },
  601. },
  602. ]);
  603. requestFilterEngine.setFiltersForTest([filter]);
  604. await requestFilterEngine.applyFinal(
  605. { provider: { id: 1, groupTag: null } } as Parameters<
  606. typeof requestFilterEngine.applyFinal
  607. >[0],
  608. body,
  609. headers
  610. );
  611. const metadata = body.metadata as Record<string, unknown>;
  612. expect(metadata.user_id).toBe("abc");
  613. expect("internal_tracking" in metadata).toBe(false);
  614. });
  615. test("deep merge on nested objects (e.g., cache_control)", async () => {
  616. const body: Record<string, unknown> = {
  617. messages: [
  618. {
  619. role: "system",
  620. content: [
  621. {
  622. type: "text",
  623. text: "prompt",
  624. cache_control: { type: "ephemeral" },
  625. },
  626. ],
  627. },
  628. ],
  629. };
  630. const headers = new Headers();
  631. const filter = createAdvancedFilter([
  632. {
  633. op: "merge",
  634. scope: "body",
  635. path: "messages[0].content[0].cache_control",
  636. value: { type: "persistent", ttl: 3600 },
  637. },
  638. ]);
  639. requestFilterEngine.setFiltersForTest([filter]);
  640. await requestFilterEngine.applyFinal(
  641. { provider: { id: 1, groupTag: null } } as Parameters<
  642. typeof requestFilterEngine.applyFinal
  643. >[0],
  644. body,
  645. headers
  646. );
  647. const msgs = body.messages as Array<{
  648. content: Array<{ cache_control: Record<string, unknown> }>;
  649. }>;
  650. expect(msgs[0].content[0].cache_control).toEqual({
  651. type: "persistent",
  652. ttl: 3600,
  653. });
  654. });
  655. test("merge creates target object if missing", async () => {
  656. const body: Record<string, unknown> = {};
  657. const headers = new Headers();
  658. const filter = createAdvancedFilter([
  659. {
  660. op: "merge",
  661. scope: "body",
  662. path: "metadata",
  663. value: { user_id: "abc" },
  664. },
  665. ]);
  666. requestFilterEngine.setFiltersForTest([filter]);
  667. await requestFilterEngine.applyFinal(
  668. { provider: { id: 1, groupTag: null } } as Parameters<
  669. typeof requestFilterEngine.applyFinal
  670. >[0],
  671. body,
  672. headers
  673. );
  674. expect((body.metadata as Record<string, string>).user_id).toBe("abc");
  675. });
  676. });
  677. // =============================================================================
  678. // Matcher Tests
  679. // =============================================================================
  680. describe("Advanced Mode - Matcher", () => {
  681. beforeEach(() => {
  682. filterId = 0;
  683. requestFilterEngine.setFiltersForTest([]);
  684. });
  685. test("contains match (string field)", async () => {
  686. const body: Record<string, unknown> = {
  687. items: [{ name: "hello world" }, { name: "goodbye" }, { name: "hello there" }],
  688. };
  689. const headers = new Headers();
  690. const filter = createAdvancedFilter([
  691. {
  692. op: "remove",
  693. scope: "body",
  694. path: "items",
  695. matcher: { field: "name", value: "hello", matchType: "contains" },
  696. },
  697. ]);
  698. requestFilterEngine.setFiltersForTest([filter]);
  699. await requestFilterEngine.applyFinal(
  700. { provider: { id: 1, groupTag: null } } as Parameters<
  701. typeof requestFilterEngine.applyFinal
  702. >[0],
  703. body,
  704. headers
  705. );
  706. const items = body.items as Array<{ name: string }>;
  707. expect(items).toHaveLength(1);
  708. expect(items[0].name).toBe("goodbye");
  709. });
  710. test("regex match (valid pattern)", async () => {
  711. const body: Record<string, unknown> = {
  712. items: [{ tag: "v1.0.0" }, { tag: "v2.0.0" }, { tag: "beta" }],
  713. };
  714. const headers = new Headers();
  715. const filter = createAdvancedFilter([
  716. {
  717. op: "remove",
  718. scope: "body",
  719. path: "items",
  720. matcher: { field: "tag", value: "^v\\d+", matchType: "regex" },
  721. },
  722. ]);
  723. requestFilterEngine.setFiltersForTest([filter]);
  724. await requestFilterEngine.applyFinal(
  725. { provider: { id: 1, groupTag: null } } as Parameters<
  726. typeof requestFilterEngine.applyFinal
  727. >[0],
  728. body,
  729. headers
  730. );
  731. const items = body.items as Array<{ tag: string }>;
  732. expect(items).toHaveLength(1);
  733. expect(items[0].tag).toBe("beta");
  734. });
  735. });
  736. // =============================================================================
  737. // Final Phase Integration
  738. // =============================================================================
  739. describe("Advanced Mode - Final Phase Integration", () => {
  740. beforeEach(() => {
  741. filterId = 0;
  742. requestFilterEngine.setFiltersForTest([]);
  743. });
  744. test("final filters execute on provided body/headers (not session)", async () => {
  745. // Simple mode guard filter modifies session
  746. const guardFilter = createFilter({
  747. ruleMode: "simple",
  748. executionPhase: "guard",
  749. scope: "header",
  750. action: "set",
  751. target: "x-guard",
  752. replacement: "from-guard",
  753. bindingType: "global",
  754. });
  755. // Advanced mode final filter modifies body/headers directly
  756. const finalFilter = createAdvancedFilter(
  757. [{ op: "set", scope: "body", path: "injected", value: true }],
  758. { bindingType: "global" }
  759. );
  760. requestFilterEngine.setFiltersForTest([guardFilter, finalFilter]);
  761. // applyFinal only processes final-phase filters
  762. const body: Record<string, unknown> = { original: "data" };
  763. const headers = new Headers();
  764. await requestFilterEngine.applyFinal(
  765. { provider: { id: 1, groupTag: null } } as Parameters<
  766. typeof requestFilterEngine.applyFinal
  767. >[0],
  768. body,
  769. headers
  770. );
  771. expect(body.injected).toBe(true);
  772. // guard filter should NOT have been applied to headers
  773. expect(headers.has("x-guard")).toBe(false);
  774. });
  775. test("transport header blacklist enforced after final ops", async () => {
  776. const body: Record<string, unknown> = {};
  777. const headers = new Headers();
  778. const filter = createAdvancedFilter(
  779. [
  780. { op: "set", scope: "header", path: "content-length", value: "999" },
  781. { op: "set", scope: "header", path: "connection", value: "keep-alive" },
  782. { op: "set", scope: "header", path: "transfer-encoding", value: "chunked" },
  783. { op: "set", scope: "header", path: "x-custom", value: "allowed" },
  784. ],
  785. { bindingType: "global" }
  786. );
  787. requestFilterEngine.setFiltersForTest([filter]);
  788. await requestFilterEngine.applyFinal(
  789. { provider: { id: 1, groupTag: null } } as Parameters<
  790. typeof requestFilterEngine.applyFinal
  791. >[0],
  792. body,
  793. headers
  794. );
  795. expect(headers.has("content-length")).toBe(false);
  796. expect(headers.has("connection")).toBe(false);
  797. expect(headers.has("transfer-encoding")).toBe(false);
  798. expect(headers.get("x-custom")).toBe("allowed");
  799. });
  800. test("provider binding works in final phase", async () => {
  801. const providerFilter = createAdvancedFilter(
  802. [{ op: "set", scope: "body", path: "provider_applied", value: true }],
  803. {
  804. bindingType: "providers",
  805. providerIds: [42],
  806. }
  807. );
  808. const otherFilter = createAdvancedFilter(
  809. [{ op: "set", scope: "body", path: "other_applied", value: true }],
  810. {
  811. bindingType: "providers",
  812. providerIds: [99],
  813. }
  814. );
  815. requestFilterEngine.setFiltersForTest([providerFilter, otherFilter]);
  816. const body: Record<string, unknown> = {};
  817. const headers = new Headers();
  818. await requestFilterEngine.applyFinal(
  819. { provider: { id: 42, groupTag: null } } as Parameters<
  820. typeof requestFilterEngine.applyFinal
  821. >[0],
  822. body,
  823. headers
  824. );
  825. expect(body.provider_applied).toBe(true);
  826. expect(body.other_applied).toBeUndefined();
  827. });
  828. test("simple mode filters in final phase use existing logic on body/headers", async () => {
  829. const filter = createFilter({
  830. ruleMode: "simple",
  831. executionPhase: "final",
  832. scope: "body",
  833. action: "json_path",
  834. target: "secret",
  835. replacement: "***",
  836. bindingType: "global",
  837. });
  838. requestFilterEngine.setFiltersForTest([filter]);
  839. const body: Record<string, unknown> = { secret: "my-api-key" };
  840. const headers = new Headers();
  841. await requestFilterEngine.applyFinal(
  842. { provider: { id: 1, groupTag: null } } as Parameters<
  843. typeof requestFilterEngine.applyFinal
  844. >[0],
  845. body,
  846. headers
  847. );
  848. expect(body.secret).toBe("***");
  849. });
  850. });