|
5 | 5 | validateAnthropicCompliance, |
6 | 6 | addInterruptedSentinel, |
7 | 7 | injectModeTransition, |
| 8 | + filterEmptyAssistantMessages, |
8 | 9 | } from "./modelMessageTransform"; |
9 | 10 | import type { CmuxMessage } from "@/types/message"; |
10 | 11 |
|
@@ -951,3 +952,156 @@ describe("injectModeTransition", () => { |
951 | 952 | }); |
952 | 953 | }); |
953 | 954 | }); |
| 955 | + |
| 956 | +describe("filterEmptyAssistantMessages", () => { |
| 957 | + it("should filter out assistant messages with only reasoning when preserveReasoningOnly=false", () => { |
| 958 | + const messages: CmuxMessage[] = [ |
| 959 | + { |
| 960 | + id: "user-1", |
| 961 | + role: "user", |
| 962 | + parts: [{ type: "text", text: "Hello" }], |
| 963 | + metadata: { timestamp: 1000 }, |
| 964 | + }, |
| 965 | + { |
| 966 | + id: "assistant-1", |
| 967 | + role: "assistant", |
| 968 | + parts: [{ type: "reasoning", text: "Let me think about this..." }], |
| 969 | + metadata: { timestamp: 2000 }, |
| 970 | + }, |
| 971 | + ]; |
| 972 | + |
| 973 | + const result = filterEmptyAssistantMessages(messages, false); |
| 974 | + |
| 975 | + // Reasoning-only message should be filtered out |
| 976 | + expect(result.length).toBe(1); |
| 977 | + expect(result[0].id).toBe("user-1"); |
| 978 | + }); |
| 979 | + |
| 980 | + it("should filter out assistant messages with empty parts array (placeholder messages)", () => { |
| 981 | + const messages: CmuxMessage[] = [ |
| 982 | + { |
| 983 | + id: "user-1", |
| 984 | + role: "user", |
| 985 | + parts: [{ type: "text", text: "Hello" }], |
| 986 | + metadata: { timestamp: 1000 }, |
| 987 | + }, |
| 988 | + { |
| 989 | + id: "assistant-1", |
| 990 | + role: "assistant", |
| 991 | + parts: [], // Empty placeholder message |
| 992 | + metadata: { timestamp: 2000 }, |
| 993 | + }, |
| 994 | + { |
| 995 | + id: "assistant-2", |
| 996 | + role: "assistant", |
| 997 | + parts: [], // Another empty placeholder |
| 998 | + metadata: { timestamp: 3000 }, |
| 999 | + }, |
| 1000 | + ]; |
| 1001 | + |
| 1002 | + // Empty messages should be filtered out regardless of preserveReasoningOnly |
| 1003 | + const result1 = filterEmptyAssistantMessages(messages, false); |
| 1004 | + expect(result1.length).toBe(1); |
| 1005 | + expect(result1[0].id).toBe("user-1"); |
| 1006 | + |
| 1007 | + const result2 = filterEmptyAssistantMessages(messages, true); |
| 1008 | + expect(result2.length).toBe(1); |
| 1009 | + expect(result2[0].id).toBe("user-1"); |
| 1010 | + }); |
| 1011 | + |
| 1012 | + it("should preserve assistant messages with only reasoning when preserveReasoningOnly=true", () => { |
| 1013 | + const messages: CmuxMessage[] = [ |
| 1014 | + { |
| 1015 | + id: "user-1", |
| 1016 | + role: "user", |
| 1017 | + parts: [{ type: "text", text: "Hello" }], |
| 1018 | + metadata: { timestamp: 1000 }, |
| 1019 | + }, |
| 1020 | + { |
| 1021 | + id: "assistant-1", |
| 1022 | + role: "assistant", |
| 1023 | + parts: [{ type: "reasoning", text: "Let me think about this..." }], |
| 1024 | + metadata: { timestamp: 2000 }, |
| 1025 | + }, |
| 1026 | + ]; |
| 1027 | + |
| 1028 | + const result = filterEmptyAssistantMessages(messages, true); |
| 1029 | + |
| 1030 | + // Reasoning-only message should be preserved when preserveReasoningOnly=true |
| 1031 | + expect(result.length).toBe(2); |
| 1032 | + expect(result[1].id).toBe("assistant-1"); |
| 1033 | + expect(result[1].parts).toEqual([{ type: "reasoning", text: "Let me think about this..." }]); |
| 1034 | + }); |
| 1035 | + |
| 1036 | + it("should preserve assistant messages with text content regardless of preserveReasoningOnly", () => { |
| 1037 | + const messages: CmuxMessage[] = [ |
| 1038 | + { |
| 1039 | + id: "assistant-1", |
| 1040 | + role: "assistant", |
| 1041 | + parts: [ |
| 1042 | + { type: "reasoning", text: "Thinking..." }, |
| 1043 | + { type: "text", text: "Here's my answer" }, |
| 1044 | + ], |
| 1045 | + metadata: { timestamp: 2000 }, |
| 1046 | + }, |
| 1047 | + ]; |
| 1048 | + |
| 1049 | + // With preserveReasoningOnly=false |
| 1050 | + const result1 = filterEmptyAssistantMessages(messages, false); |
| 1051 | + expect(result1.length).toBe(1); |
| 1052 | + expect(result1[0].id).toBe("assistant-1"); |
| 1053 | + |
| 1054 | + // With preserveReasoningOnly=true |
| 1055 | + const result2 = filterEmptyAssistantMessages(messages, true); |
| 1056 | + expect(result2.length).toBe(1); |
| 1057 | + expect(result2[0].id).toBe("assistant-1"); |
| 1058 | + }); |
| 1059 | + |
| 1060 | + it("should filter out assistant messages with only empty text regardless of preserveReasoningOnly", () => { |
| 1061 | + const messages: CmuxMessage[] = [ |
| 1062 | + { |
| 1063 | + id: "assistant-1", |
| 1064 | + role: "assistant", |
| 1065 | + parts: [{ type: "text", text: "" }], |
| 1066 | + metadata: { timestamp: 2000 }, |
| 1067 | + }, |
| 1068 | + ]; |
| 1069 | + |
| 1070 | + // With preserveReasoningOnly=false |
| 1071 | + const result1 = filterEmptyAssistantMessages(messages, false); |
| 1072 | + expect(result1.length).toBe(0); |
| 1073 | + |
| 1074 | + // With preserveReasoningOnly=true |
| 1075 | + const result2 = filterEmptyAssistantMessages(messages, true); |
| 1076 | + expect(result2.length).toBe(0); |
| 1077 | + }); |
| 1078 | + |
| 1079 | + it("should preserve messages interrupted during thinking phase when preserveReasoningOnly=true", () => { |
| 1080 | + // Simulates an interrupted stream during Extended Thinking |
| 1081 | + const messages: CmuxMessage[] = [ |
| 1082 | + { |
| 1083 | + id: "user-1", |
| 1084 | + role: "user", |
| 1085 | + parts: [{ type: "text", text: "Solve this problem" }], |
| 1086 | + metadata: { timestamp: 1000 }, |
| 1087 | + }, |
| 1088 | + { |
| 1089 | + id: "assistant-1", |
| 1090 | + role: "assistant", |
| 1091 | + parts: [{ type: "reasoning", text: "Let me analyze this step by step..." }], |
| 1092 | + metadata: { timestamp: 2000, partial: true }, |
| 1093 | + }, |
| 1094 | + ]; |
| 1095 | + |
| 1096 | + // When thinking is disabled, filter out reasoning-only message |
| 1097 | + const result1 = filterEmptyAssistantMessages(messages, false); |
| 1098 | + expect(result1.length).toBe(1); |
| 1099 | + expect(result1[0].id).toBe("user-1"); |
| 1100 | + |
| 1101 | + // When thinking is enabled, preserve it for API compliance |
| 1102 | + const result2 = filterEmptyAssistantMessages(messages, true); |
| 1103 | + expect(result2.length).toBe(2); |
| 1104 | + expect(result2[1].id).toBe("assistant-1"); |
| 1105 | + expect(result2[1].metadata?.partial).toBe(true); |
| 1106 | + }); |
| 1107 | +}); |
0 commit comments