response-input-rectifier.test.ts 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. import { describe, expect, it, vi } from "vitest";
  2. import {
  3. normalizeResponseInput,
  4. rectifyResponseInput,
  5. } from "@/app/v1/_lib/proxy/response-input-rectifier";
  6. import type { ProxySession } from "@/app/v1/_lib/proxy/session";
  7. import type { SpecialSetting } from "@/types/special-settings";
  8. vi.mock("@/lib/config/system-settings-cache", () => ({
  9. getCachedSystemSettings: vi.fn(),
  10. }));
  11. vi.mock("@/lib/logger", () => ({
  12. logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
  13. }));
  14. const { getCachedSystemSettings } = await import("@/lib/config/system-settings-cache");
  15. const getCachedMock = vi.mocked(getCachedSystemSettings);
  16. function createMockSession(input: unknown): {
  17. session: ProxySession;
  18. specialSettings: SpecialSetting[];
  19. } {
  20. const specialSettings: SpecialSetting[] = [];
  21. const session = {
  22. request: { message: { model: "gpt-4o", input } },
  23. sessionId: "sess_test",
  24. addSpecialSetting: (s: SpecialSetting) => specialSettings.push(s),
  25. } as unknown as ProxySession;
  26. return { session, specialSettings };
  27. }
  28. describe("rectifyResponseInput", () => {
  29. // --- Passthrough cases ---
  30. it("passes through array input unchanged", () => {
  31. const message: Record<string, unknown> = {
  32. model: "gpt-4o",
  33. input: [{ role: "user", content: [{ type: "input_text", text: "hi" }] }],
  34. };
  35. const original = message.input;
  36. const result = rectifyResponseInput(message);
  37. expect(result).toEqual({ applied: false, action: "passthrough", originalType: "array" });
  38. expect(message.input).toBe(original);
  39. });
  40. it("passes through empty array input unchanged", () => {
  41. const message: Record<string, unknown> = { input: [] };
  42. const result = rectifyResponseInput(message);
  43. expect(result).toEqual({ applied: false, action: "passthrough", originalType: "array" });
  44. expect(message.input).toEqual([]);
  45. });
  46. it("passes through undefined input", () => {
  47. const message: Record<string, unknown> = { model: "gpt-4o" };
  48. const result = rectifyResponseInput(message);
  49. expect(result).toEqual({ applied: false, action: "passthrough", originalType: "other" });
  50. expect(message.input).toBeUndefined();
  51. });
  52. it("passes through null input", () => {
  53. const message: Record<string, unknown> = { input: null };
  54. const result = rectifyResponseInput(message);
  55. expect(result).toEqual({ applied: false, action: "passthrough", originalType: "other" });
  56. expect(message.input).toBeNull();
  57. });
  58. // --- String normalization ---
  59. it("normalizes non-empty string to user message array", () => {
  60. const message: Record<string, unknown> = { model: "gpt-4o", input: "hello world" };
  61. const result = rectifyResponseInput(message);
  62. expect(result).toEqual({ applied: true, action: "string_to_array", originalType: "string" });
  63. expect(message.input).toEqual([
  64. {
  65. role: "user",
  66. content: [{ type: "input_text", text: "hello world" }],
  67. },
  68. ]);
  69. });
  70. it("normalizes empty string to empty array", () => {
  71. const message: Record<string, unknown> = { input: "" };
  72. const result = rectifyResponseInput(message);
  73. expect(result).toEqual({
  74. applied: true,
  75. action: "empty_string_to_empty_array",
  76. originalType: "string",
  77. });
  78. expect(message.input).toEqual([]);
  79. });
  80. it("normalizes whitespace-only string to user message (not empty)", () => {
  81. const message: Record<string, unknown> = { input: " " };
  82. const result = rectifyResponseInput(message);
  83. expect(result).toEqual({ applied: true, action: "string_to_array", originalType: "string" });
  84. expect(message.input).toEqual([
  85. {
  86. role: "user",
  87. content: [{ type: "input_text", text: " " }],
  88. },
  89. ]);
  90. });
  91. it("normalizes multiline string", () => {
  92. const message: Record<string, unknown> = { input: "line1\nline2\nline3" };
  93. const result = rectifyResponseInput(message);
  94. expect(result).toEqual({ applied: true, action: "string_to_array", originalType: "string" });
  95. expect(message.input).toEqual([
  96. {
  97. role: "user",
  98. content: [{ type: "input_text", text: "line1\nline2\nline3" }],
  99. },
  100. ]);
  101. });
  102. // --- Object normalization ---
  103. it("wraps single MessageInput (has role) into array", () => {
  104. const inputObj = { role: "user", content: [{ type: "input_text", text: "hi" }] };
  105. const message: Record<string, unknown> = { input: inputObj };
  106. const result = rectifyResponseInput(message);
  107. expect(result).toEqual({ applied: true, action: "object_to_array", originalType: "object" });
  108. expect(message.input).toEqual([inputObj]);
  109. });
  110. it("wraps single ToolOutputsInput (has type) into array", () => {
  111. const inputObj = { type: "function_call_output", call_id: "call_123", output: "result" };
  112. const message: Record<string, unknown> = { input: inputObj };
  113. const result = rectifyResponseInput(message);
  114. expect(result).toEqual({ applied: true, action: "object_to_array", originalType: "object" });
  115. expect(message.input).toEqual([inputObj]);
  116. });
  117. it("passes through object without role or type", () => {
  118. const message: Record<string, unknown> = { input: { foo: "bar", baz: 42 } };
  119. const result = rectifyResponseInput(message);
  120. expect(result).toEqual({ applied: false, action: "passthrough", originalType: "other" });
  121. });
  122. // --- Edge cases ---
  123. it("does not modify other message fields", () => {
  124. const message: Record<string, unknown> = {
  125. model: "gpt-4o",
  126. input: "hello",
  127. temperature: 0.7,
  128. stream: true,
  129. };
  130. rectifyResponseInput(message);
  131. expect(message.model).toBe("gpt-4o");
  132. expect(message.temperature).toBe(0.7);
  133. expect(message.stream).toBe(true);
  134. });
  135. it("passes through numeric input as other", () => {
  136. const message: Record<string, unknown> = { input: 42 };
  137. const result = rectifyResponseInput(message);
  138. expect(result).toEqual({ applied: false, action: "passthrough", originalType: "other" });
  139. });
  140. it("passes through boolean input as other", () => {
  141. const message: Record<string, unknown> = { input: true };
  142. const result = rectifyResponseInput(message);
  143. expect(result).toEqual({ applied: false, action: "passthrough", originalType: "other" });
  144. });
  145. });
  146. describe("normalizeResponseInput", () => {
  147. it("normalizes string input and records audit when enabled", async () => {
  148. getCachedMock.mockResolvedValue({ enableResponseInputRectifier: true } as any);
  149. const { session, specialSettings } = createMockSession("hello");
  150. await normalizeResponseInput(session);
  151. const message = session.request.message as Record<string, unknown>;
  152. expect(message.input).toEqual([
  153. { role: "user", content: [{ type: "input_text", text: "hello" }] },
  154. ]);
  155. expect(specialSettings).toHaveLength(1);
  156. expect(specialSettings[0]).toMatchObject({
  157. type: "response_input_rectifier",
  158. hit: true,
  159. action: "string_to_array",
  160. originalType: "string",
  161. });
  162. });
  163. it("skips normalization when feature is disabled", async () => {
  164. getCachedMock.mockResolvedValue({ enableResponseInputRectifier: false } as any);
  165. const { session, specialSettings } = createMockSession("hello");
  166. await normalizeResponseInput(session);
  167. const message = session.request.message as Record<string, unknown>;
  168. expect(message.input).toBe("hello");
  169. expect(specialSettings).toHaveLength(0);
  170. });
  171. it("does not record audit for passthrough (array input)", async () => {
  172. getCachedMock.mockResolvedValue({ enableResponseInputRectifier: true } as any);
  173. const arrayInput = [{ role: "user", content: [{ type: "input_text", text: "hi" }] }];
  174. const { session, specialSettings } = createMockSession(arrayInput);
  175. await normalizeResponseInput(session);
  176. const message = session.request.message as Record<string, unknown>;
  177. expect(message.input).toBe(arrayInput);
  178. expect(specialSettings).toHaveLength(0);
  179. });
  180. it("wraps single object input and records audit when enabled", async () => {
  181. getCachedMock.mockResolvedValue({ enableResponseInputRectifier: true } as any);
  182. const inputObj = { role: "user", content: [{ type: "input_text", text: "hi" }] };
  183. const { session, specialSettings } = createMockSession(inputObj);
  184. await normalizeResponseInput(session);
  185. const message = session.request.message as Record<string, unknown>;
  186. expect(message.input).toEqual([inputObj]);
  187. expect(specialSettings).toHaveLength(1);
  188. expect(specialSettings[0]).toMatchObject({
  189. type: "response_input_rectifier",
  190. action: "object_to_array",
  191. originalType: "object",
  192. });
  193. });
  194. });