endpoint-policy-parity.test.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. import { beforeEach, describe, expect, test, vi } from "vitest";
  2. import {
  3. type EndpointPolicy,
  4. isRawPassthroughEndpointPath,
  5. isRawPassthroughEndpointPolicy,
  6. resolveEndpointPolicy,
  7. } from "@/app/v1/_lib/proxy/endpoint-policy";
  8. import { V1_ENDPOINT_PATHS } from "@/app/v1/_lib/proxy/endpoint-paths";
  9. // ---------------------------------------------------------------------------
  10. // Shared constants
  11. // ---------------------------------------------------------------------------
  12. const RAW_PASSTHROUGH_ENDPOINTS = [
  13. V1_ENDPOINT_PATHS.MESSAGES_COUNT_TOKENS,
  14. V1_ENDPOINT_PATHS.RESPONSES_COMPACT,
  15. ] as const;
  16. const DEFAULT_ENDPOINTS = [
  17. V1_ENDPOINT_PATHS.MESSAGES,
  18. V1_ENDPOINT_PATHS.RESPONSES,
  19. V1_ENDPOINT_PATHS.CHAT_COMPLETIONS,
  20. ] as const;
  21. // ---------------------------------------------------------------------------
  22. // T11: Endpoint parity -- count_tokens and responses/compact produce
  23. // identical EndpointPolicy objects and exhibit identical behaviour
  24. // under provider errors.
  25. // ---------------------------------------------------------------------------
  26. describe("T11: raw passthrough endpoint parity", () => {
  27. test("count_tokens and responses/compact resolve to the exact same EndpointPolicy object", () => {
  28. const countTokensPolicy = resolveEndpointPolicy(V1_ENDPOINT_PATHS.MESSAGES_COUNT_TOKENS);
  29. const compactPolicy = resolveEndpointPolicy(V1_ENDPOINT_PATHS.RESPONSES_COMPACT);
  30. // Reference equality: same frozen singleton
  31. expect(countTokensPolicy).toBe(compactPolicy);
  32. // Both recognized as raw_passthrough
  33. expect(isRawPassthroughEndpointPolicy(countTokensPolicy)).toBe(true);
  34. expect(isRawPassthroughEndpointPolicy(compactPolicy)).toBe(true);
  35. });
  36. test("both raw passthrough endpoints have identical strict policy fields", () => {
  37. const countTokensPolicy = resolveEndpointPolicy(V1_ENDPOINT_PATHS.MESSAGES_COUNT_TOKENS);
  38. const compactPolicy = resolveEndpointPolicy(V1_ENDPOINT_PATHS.RESPONSES_COMPACT);
  39. const expectedPolicy: EndpointPolicy = {
  40. kind: "raw_passthrough",
  41. guardPreset: "raw_passthrough",
  42. allowRetry: false,
  43. allowProviderSwitch: false,
  44. allowCircuitBreakerAccounting: false,
  45. trackConcurrentRequests: false,
  46. bypassRequestFilters: true,
  47. bypassForwarderPreprocessing: true,
  48. bypassSpecialSettings: true,
  49. bypassResponseRectifier: true,
  50. endpointPoolStrictness: "strict",
  51. };
  52. expect(countTokensPolicy).toEqual(expectedPolicy);
  53. expect(compactPolicy).toEqual(expectedPolicy);
  54. });
  55. test("under provider error, both endpoints result in no retry, no provider switch, no circuit breaker accounting", () => {
  56. for (const pathname of RAW_PASSTHROUGH_ENDPOINTS) {
  57. const policy = resolveEndpointPolicy(pathname);
  58. expect(policy.allowRetry).toBe(false);
  59. expect(policy.allowProviderSwitch).toBe(false);
  60. expect(policy.allowCircuitBreakerAccounting).toBe(false);
  61. }
  62. });
  63. test("isRawPassthroughEndpointPath returns true for both raw passthrough canonical paths", () => {
  64. for (const pathname of RAW_PASSTHROUGH_ENDPOINTS) {
  65. expect(isRawPassthroughEndpointPath(pathname)).toBe(true);
  66. }
  67. });
  68. });
  69. // ---------------------------------------------------------------------------
  70. // T12: Bypass completeness -- spy-based zero-call assertions to verify that
  71. // request filter guards early-return without invoking the engine.
  72. // ---------------------------------------------------------------------------
  73. const applyGlobalMock = vi.fn(async () => {});
  74. const applyForProviderMock = vi.fn(async () => {});
  75. vi.mock("@/lib/request-filter-engine", () => ({
  76. requestFilterEngine: {
  77. applyGlobal: applyGlobalMock,
  78. applyForProvider: applyForProviderMock,
  79. },
  80. }));
  81. vi.mock("@/lib/logger", () => ({
  82. logger: {
  83. debug: vi.fn(),
  84. info: vi.fn(),
  85. warn: vi.fn(),
  86. error: vi.fn(),
  87. trace: vi.fn(),
  88. fatal: vi.fn(),
  89. },
  90. }));
  91. describe("T12: bypass completeness (spy-based zero-call assertions)", () => {
  92. beforeEach(() => {
  93. applyGlobalMock.mockClear();
  94. applyForProviderMock.mockClear();
  95. });
  96. test("ProxyRequestFilter.ensure early-returns without calling applyGlobal for raw passthrough", async () => {
  97. const { ProxyRequestFilter } = await import("@/app/v1/_lib/proxy/request-filter");
  98. for (const pathname of RAW_PASSTHROUGH_ENDPOINTS) {
  99. applyGlobalMock.mockClear();
  100. const session = {
  101. getEndpointPolicy: () => resolveEndpointPolicy(pathname),
  102. } as any;
  103. await ProxyRequestFilter.ensure(session);
  104. expect(applyGlobalMock).not.toHaveBeenCalled();
  105. }
  106. });
  107. test("ProxyProviderRequestFilter.ensure early-returns without calling applyForProvider for raw passthrough", async () => {
  108. const { ProxyProviderRequestFilter } = await import(
  109. "@/app/v1/_lib/proxy/provider-request-filter"
  110. );
  111. for (const pathname of RAW_PASSTHROUGH_ENDPOINTS) {
  112. applyForProviderMock.mockClear();
  113. const session = {
  114. getEndpointPolicy: () => resolveEndpointPolicy(pathname),
  115. provider: { id: 1 },
  116. } as any;
  117. await ProxyProviderRequestFilter.ensure(session);
  118. expect(applyForProviderMock).not.toHaveBeenCalled();
  119. }
  120. });
  121. test("ProxyRequestFilter.ensure calls applyGlobal for default policy endpoints", async () => {
  122. const { ProxyRequestFilter } = await import("@/app/v1/_lib/proxy/request-filter");
  123. for (const pathname of DEFAULT_ENDPOINTS) {
  124. applyGlobalMock.mockClear();
  125. const session = {
  126. getEndpointPolicy: () => resolveEndpointPolicy(pathname),
  127. } as any;
  128. await ProxyRequestFilter.ensure(session);
  129. expect(applyGlobalMock).toHaveBeenCalledTimes(1);
  130. }
  131. });
  132. test("ProxyProviderRequestFilter.ensure calls applyForProvider for default policy endpoints", async () => {
  133. const { ProxyProviderRequestFilter } = await import(
  134. "@/app/v1/_lib/proxy/provider-request-filter"
  135. );
  136. for (const pathname of DEFAULT_ENDPOINTS) {
  137. applyForProviderMock.mockClear();
  138. const session = {
  139. getEndpointPolicy: () => resolveEndpointPolicy(pathname),
  140. provider: { id: 1 },
  141. } as any;
  142. await ProxyProviderRequestFilter.ensure(session);
  143. expect(applyForProviderMock).toHaveBeenCalledTimes(1);
  144. }
  145. });
  146. });
  147. // ---------------------------------------------------------------------------
  148. // T13: Non-target regression -- default endpoints retain full default policy.
  149. // ---------------------------------------------------------------------------
  150. describe("T13: non-target regression (default policy preserved)", () => {
  151. const expectedDefaultPolicy: EndpointPolicy = {
  152. kind: "default",
  153. guardPreset: "chat",
  154. allowRetry: true,
  155. allowProviderSwitch: true,
  156. allowCircuitBreakerAccounting: true,
  157. trackConcurrentRequests: true,
  158. bypassRequestFilters: false,
  159. bypassForwarderPreprocessing: false,
  160. bypassSpecialSettings: false,
  161. bypassResponseRectifier: false,
  162. endpointPoolStrictness: "inherit",
  163. };
  164. test("/v1/messages retains full default policy", () => {
  165. const policy = resolveEndpointPolicy(V1_ENDPOINT_PATHS.MESSAGES);
  166. expect(policy).toEqual(expectedDefaultPolicy);
  167. expect(isRawPassthroughEndpointPolicy(policy)).toBe(false);
  168. });
  169. test("/v1/responses retains full default policy", () => {
  170. const policy = resolveEndpointPolicy(V1_ENDPOINT_PATHS.RESPONSES);
  171. expect(policy).toEqual(expectedDefaultPolicy);
  172. expect(isRawPassthroughEndpointPolicy(policy)).toBe(false);
  173. });
  174. test("/v1/chat/completions retains full default policy", () => {
  175. const policy = resolveEndpointPolicy(V1_ENDPOINT_PATHS.CHAT_COMPLETIONS);
  176. expect(policy).toEqual(expectedDefaultPolicy);
  177. expect(isRawPassthroughEndpointPolicy(policy)).toBe(false);
  178. });
  179. test("all default endpoints resolve to the same singleton object", () => {
  180. const policies = DEFAULT_ENDPOINTS.map((p) => resolveEndpointPolicy(p));
  181. // All should be the same reference
  182. for (let i = 1; i < policies.length; i++) {
  183. expect(policies[i]).toBe(policies[0]);
  184. }
  185. });
  186. test("default policy has all bypass flags set to false", () => {
  187. for (const pathname of DEFAULT_ENDPOINTS) {
  188. const policy = resolveEndpointPolicy(pathname);
  189. expect(policy.bypassRequestFilters).toBe(false);
  190. expect(policy.bypassForwarderPreprocessing).toBe(false);
  191. expect(policy.bypassSpecialSettings).toBe(false);
  192. expect(policy.bypassResponseRectifier).toBe(false);
  193. }
  194. });
  195. test("default policy has all allow flags set to true", () => {
  196. for (const pathname of DEFAULT_ENDPOINTS) {
  197. const policy = resolveEndpointPolicy(pathname);
  198. expect(policy.allowRetry).toBe(true);
  199. expect(policy.allowProviderSwitch).toBe(true);
  200. expect(policy.allowCircuitBreakerAccounting).toBe(true);
  201. expect(policy.trackConcurrentRequests).toBe(true);
  202. }
  203. });
  204. });
  205. // ---------------------------------------------------------------------------
  206. // T14: Path edge-case tests -- normalization handles trailing slashes, case
  207. // variants, query strings, and non-matching paths correctly.
  208. // ---------------------------------------------------------------------------
  209. describe("T14: path edge-case normalization", () => {
  210. test("trailing slash: /v1/messages/count_tokens/ -> raw_passthrough", () => {
  211. expect(isRawPassthroughEndpointPath("/v1/messages/count_tokens/")).toBe(true);
  212. const policy = resolveEndpointPolicy("/v1/messages/count_tokens/");
  213. expect(policy.kind).toBe("raw_passthrough");
  214. });
  215. test("trailing slash: /v1/responses/compact/ -> raw_passthrough", () => {
  216. expect(isRawPassthroughEndpointPath("/v1/responses/compact/")).toBe(true);
  217. const policy = resolveEndpointPolicy("/v1/responses/compact/");
  218. expect(policy.kind).toBe("raw_passthrough");
  219. });
  220. test("uppercase: /V1/MESSAGES/COUNT_TOKENS -> raw_passthrough", () => {
  221. expect(isRawPassthroughEndpointPath("/V1/MESSAGES/COUNT_TOKENS")).toBe(true);
  222. const policy = resolveEndpointPolicy("/V1/MESSAGES/COUNT_TOKENS");
  223. expect(policy.kind).toBe("raw_passthrough");
  224. });
  225. test("uppercase: /V1/RESPONSES/COMPACT -> raw_passthrough", () => {
  226. expect(isRawPassthroughEndpointPath("/V1/RESPONSES/COMPACT")).toBe(true);
  227. const policy = resolveEndpointPolicy("/V1/RESPONSES/COMPACT");
  228. expect(policy.kind).toBe("raw_passthrough");
  229. });
  230. test("query string: /v1/messages/count_tokens?foo=bar -> raw_passthrough", () => {
  231. expect(isRawPassthroughEndpointPath("/v1/messages/count_tokens?foo=bar")).toBe(true);
  232. const policy = resolveEndpointPolicy("/v1/messages/count_tokens?foo=bar");
  233. expect(policy.kind).toBe("raw_passthrough");
  234. });
  235. test("query string: /v1/responses/compact?foo=bar -> raw_passthrough", () => {
  236. expect(isRawPassthroughEndpointPath("/v1/responses/compact?foo=bar")).toBe(true);
  237. const policy = resolveEndpointPolicy("/v1/responses/compact?foo=bar");
  238. expect(policy.kind).toBe("raw_passthrough");
  239. });
  240. test("combined edge case: uppercase + trailing slash + query string", () => {
  241. expect(isRawPassthroughEndpointPath("/V1/MESSAGES/COUNT_TOKENS/?x=1")).toBe(true);
  242. expect(isRawPassthroughEndpointPath("/V1/RESPONSES/COMPACT/?x=1")).toBe(true);
  243. const policy1 = resolveEndpointPolicy("/V1/MESSAGES/COUNT_TOKENS/?x=1");
  244. const policy2 = resolveEndpointPolicy("/V1/RESPONSES/COMPACT/?x=1");
  245. expect(policy1.kind).toBe("raw_passthrough");
  246. expect(policy2.kind).toBe("raw_passthrough");
  247. });
  248. test("/v1/messages/ (with trailing slash) -> default, NOT raw_passthrough", () => {
  249. expect(isRawPassthroughEndpointPath("/v1/messages/")).toBe(false);
  250. const policy = resolveEndpointPolicy("/v1/messages/");
  251. expect(policy.kind).toBe("default");
  252. });
  253. test("/v1/messages (no trailing slash) -> default", () => {
  254. expect(isRawPassthroughEndpointPath("/v1/messages")).toBe(false);
  255. const policy = resolveEndpointPolicy("/v1/messages");
  256. expect(policy.kind).toBe("default");
  257. });
  258. test("/v1/responses (no sub-path) -> default", () => {
  259. expect(isRawPassthroughEndpointPath("/v1/responses")).toBe(false);
  260. const policy = resolveEndpointPolicy("/v1/responses");
  261. expect(policy.kind).toBe("default");
  262. });
  263. test("/v1/chat/completions -> default", () => {
  264. expect(isRawPassthroughEndpointPath("/v1/chat/completions")).toBe(false);
  265. const policy = resolveEndpointPolicy("/v1/chat/completions");
  266. expect(policy.kind).toBe("default");
  267. });
  268. test.each([
  269. "/v1/messages/count",
  270. "/v1/messages/count_token",
  271. "/v1/responses/mini",
  272. "/v1/responses/compacted",
  273. "/v2/messages/count_tokens",
  274. "/v1/messages/count_tokens/extra",
  275. ])("non-matching path %s -> default", (pathname) => {
  276. expect(isRawPassthroughEndpointPath(pathname)).toBe(false);
  277. const policy = resolveEndpointPolicy(pathname);
  278. expect(policy.kind).toBe("default");
  279. });
  280. test("empty and root paths -> default", () => {
  281. expect(resolveEndpointPolicy("/").kind).toBe("default");
  282. expect(resolveEndpointPolicy("").kind).toBe("default");
  283. });
  284. });