@lobehub/chat 1.136.13 → 1.137.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/.cursor/rules/add-setting-env.mdc +175 -0
  2. package/.cursor/rules/db-migrations.mdc +25 -0
  3. package/.env.example +7 -0
  4. package/CHANGELOG.md +50 -0
  5. package/Dockerfile +3 -2
  6. package/Dockerfile.database +15 -3
  7. package/Dockerfile.pglite +3 -2
  8. package/changelog/v1.json +18 -0
  9. package/docs/development/database-schema.dbml +1 -0
  10. package/docs/self-hosting/advanced/feature-flags.mdx +25 -15
  11. package/docs/self-hosting/advanced/feature-flags.zh-CN.mdx +25 -15
  12. package/docs/self-hosting/environment-variables/basic.mdx +12 -0
  13. package/docs/self-hosting/environment-variables/basic.zh-CN.mdx +12 -0
  14. package/locales/ar/setting.json +8 -0
  15. package/locales/bg-BG/setting.json +8 -0
  16. package/locales/de-DE/setting.json +8 -0
  17. package/locales/en-US/setting.json +8 -0
  18. package/locales/es-ES/setting.json +8 -0
  19. package/locales/fa-IR/setting.json +8 -0
  20. package/locales/fr-FR/setting.json +8 -0
  21. package/locales/it-IT/setting.json +8 -0
  22. package/locales/ja-JP/setting.json +8 -0
  23. package/locales/ko-KR/setting.json +8 -0
  24. package/locales/nl-NL/setting.json +8 -0
  25. package/locales/pl-PL/setting.json +8 -0
  26. package/locales/pt-BR/setting.json +8 -0
  27. package/locales/ru-RU/setting.json +8 -0
  28. package/locales/tr-TR/setting.json +8 -0
  29. package/locales/vi-VN/setting.json +8 -0
  30. package/locales/zh-CN/setting.json +8 -0
  31. package/locales/zh-TW/setting.json +8 -0
  32. package/package.json +1 -1
  33. package/packages/agent-runtime/examples/tools-calling.ts +4 -3
  34. package/packages/agent-runtime/src/core/__tests__/runtime.test.ts +559 -29
  35. package/packages/agent-runtime/src/core/runtime.ts +171 -43
  36. package/packages/agent-runtime/src/types/instruction.ts +32 -6
  37. package/packages/agent-runtime/src/types/runtime.ts +2 -2
  38. package/packages/agent-runtime/src/types/state.ts +1 -8
  39. package/packages/agent-runtime/vitest.config.mts +14 -0
  40. package/packages/const/src/settings/image.ts +8 -0
  41. package/packages/const/src/settings/index.ts +3 -0
  42. package/packages/context-engine/src/__tests__/pipeline.test.ts +485 -0
  43. package/packages/context-engine/src/base/__tests__/BaseProcessor.test.ts +381 -0
  44. package/packages/context-engine/src/base/__tests__/BaseProvider.test.ts +392 -0
  45. package/packages/context-engine/src/processors/__tests__/MessageCleanup.test.ts +346 -0
  46. package/packages/context-engine/src/processors/__tests__/ToolCall.test.ts +552 -0
  47. package/packages/database/migrations/0038_add_image_user_settings.sql +1 -0
  48. package/packages/database/migrations/meta/0038_snapshot.json +7580 -0
  49. package/packages/database/migrations/meta/_journal.json +7 -0
  50. package/packages/database/src/core/migrations.json +6 -0
  51. package/packages/database/src/models/user.ts +3 -1
  52. package/packages/database/src/schemas/user.ts +1 -0
  53. package/packages/file-loaders/src/loaders/docx/index.test.ts +0 -1
  54. package/packages/file-loaders/src/loaders/excel/__snapshots__/index.test.ts.snap +30 -0
  55. package/packages/file-loaders/src/loaders/excel/index.test.ts +8 -0
  56. package/packages/file-loaders/src/loaders/pptx/index.test.ts +25 -0
  57. package/packages/file-loaders/src/utils/parser-utils.test.ts +155 -0
  58. package/packages/file-loaders/vitest.config.mts +8 -0
  59. package/packages/model-runtime/CLAUDE.md +5 -0
  60. package/packages/model-runtime/docs/test-coverage.md +706 -0
  61. package/packages/model-runtime/src/core/ModelRuntime.test.ts +231 -0
  62. package/packages/model-runtime/src/core/RouterRuntime/createRuntime.ts +1 -1
  63. package/packages/model-runtime/src/core/openaiCompatibleFactory/createImage.test.ts +799 -0
  64. package/packages/model-runtime/src/core/openaiCompatibleFactory/index.test.ts +188 -4
  65. package/packages/model-runtime/src/core/openaiCompatibleFactory/index.ts +41 -10
  66. package/packages/model-runtime/src/core/streams/openai/__snapshots__/responsesStream.test.ts.snap +439 -0
  67. package/packages/model-runtime/src/core/streams/openai/openai.test.ts +789 -0
  68. package/packages/model-runtime/src/core/streams/openai/responsesStream.test.ts +551 -0
  69. package/packages/model-runtime/src/core/usageConverters/utils/computeChatCost.test.ts +230 -0
  70. package/packages/model-runtime/src/core/usageConverters/utils/computeImageCost.test.ts +334 -37
  71. package/packages/model-runtime/src/providerTestUtils.ts +148 -145
  72. package/packages/model-runtime/src/providers/ai302/index.test.ts +60 -0
  73. package/packages/model-runtime/src/providers/ai302/index.ts +9 -4
  74. package/packages/model-runtime/src/providers/ai360/index.test.ts +1213 -1
  75. package/packages/model-runtime/src/providers/ai360/index.ts +9 -4
  76. package/packages/model-runtime/src/providers/aihubmix/index.test.ts +73 -0
  77. package/packages/model-runtime/src/providers/aihubmix/index.ts +6 -9
  78. package/packages/model-runtime/src/providers/akashchat/index.test.ts +433 -3
  79. package/packages/model-runtime/src/providers/akashchat/index.ts +12 -7
  80. package/packages/model-runtime/src/providers/anthropic/generateObject.test.ts +183 -29
  81. package/packages/model-runtime/src/providers/anthropic/generateObject.ts +40 -24
  82. package/packages/model-runtime/src/providers/azureai/index.test.ts +102 -0
  83. package/packages/model-runtime/src/providers/baichuan/index.test.ts +416 -26
  84. package/packages/model-runtime/src/providers/baichuan/index.ts +23 -20
  85. package/packages/model-runtime/src/providers/bedrock/index.test.ts +420 -2
  86. package/packages/model-runtime/src/providers/cerebras/index.test.ts +465 -0
  87. package/packages/model-runtime/src/providers/cerebras/index.ts +8 -3
  88. package/packages/model-runtime/src/providers/cohere/index.test.ts +1074 -1
  89. package/packages/model-runtime/src/providers/cohere/index.ts +8 -3
  90. package/packages/model-runtime/src/providers/cometapi/index.test.ts +439 -3
  91. package/packages/model-runtime/src/providers/cometapi/index.ts +8 -3
  92. package/packages/model-runtime/src/providers/deepseek/index.test.ts +116 -1
  93. package/packages/model-runtime/src/providers/deepseek/index.ts +8 -3
  94. package/packages/model-runtime/src/providers/fireworksai/index.test.ts +264 -3
  95. package/packages/model-runtime/src/providers/fireworksai/index.ts +8 -3
  96. package/packages/model-runtime/src/providers/giteeai/index.test.ts +325 -3
  97. package/packages/model-runtime/src/providers/giteeai/index.ts +23 -6
  98. package/packages/model-runtime/src/providers/github/index.test.ts +532 -3
  99. package/packages/model-runtime/src/providers/github/index.ts +8 -3
  100. package/packages/model-runtime/src/providers/groq/index.test.ts +344 -31
  101. package/packages/model-runtime/src/providers/groq/index.ts +8 -3
  102. package/packages/model-runtime/src/providers/higress/index.test.ts +142 -0
  103. package/packages/model-runtime/src/providers/higress/index.ts +8 -3
  104. package/packages/model-runtime/src/providers/huggingface/index.test.ts +612 -1
  105. package/packages/model-runtime/src/providers/huggingface/index.ts +9 -4
  106. package/packages/model-runtime/src/providers/hunyuan/index.test.ts +365 -1
  107. package/packages/model-runtime/src/providers/hunyuan/index.ts +9 -3
  108. package/packages/model-runtime/src/providers/infiniai/index.test.ts +71 -0
  109. package/packages/model-runtime/src/providers/internlm/index.test.ts +369 -2
  110. package/packages/model-runtime/src/providers/internlm/index.ts +10 -5
  111. package/packages/model-runtime/src/providers/jina/index.test.ts +164 -3
  112. package/packages/model-runtime/src/providers/jina/index.ts +8 -3
  113. package/packages/model-runtime/src/providers/lmstudio/index.test.ts +182 -3
  114. package/packages/model-runtime/src/providers/lmstudio/index.ts +8 -3
  115. package/packages/model-runtime/src/providers/mistral/index.test.ts +779 -27
  116. package/packages/model-runtime/src/providers/mistral/index.ts +8 -3
  117. package/packages/model-runtime/src/providers/modelscope/index.test.ts +232 -1
  118. package/packages/model-runtime/src/providers/modelscope/index.ts +8 -3
  119. package/packages/model-runtime/src/providers/moonshot/index.test.ts +489 -2
  120. package/packages/model-runtime/src/providers/moonshot/index.ts +8 -3
  121. package/packages/model-runtime/src/providers/nebius/index.test.ts +381 -3
  122. package/packages/model-runtime/src/providers/nebius/index.ts +8 -3
  123. package/packages/model-runtime/src/providers/newapi/index.test.ts +667 -3
  124. package/packages/model-runtime/src/providers/newapi/index.ts +6 -3
  125. package/packages/model-runtime/src/providers/nvidia/index.test.ts +168 -1
  126. package/packages/model-runtime/src/providers/nvidia/index.ts +12 -7
  127. package/packages/model-runtime/src/providers/ollama/index.test.ts +797 -1
  128. package/packages/model-runtime/src/providers/ollama/index.ts +8 -0
  129. package/packages/model-runtime/src/providers/ollamacloud/index.test.ts +411 -0
  130. package/packages/model-runtime/src/providers/ollamacloud/index.ts +8 -3
  131. package/packages/model-runtime/src/providers/openai/index.test.ts +171 -2
  132. package/packages/model-runtime/src/providers/openai/index.ts +8 -3
  133. package/packages/model-runtime/src/providers/openrouter/index.test.ts +1647 -95
  134. package/packages/model-runtime/src/providers/openrouter/index.ts +12 -7
  135. package/packages/model-runtime/src/providers/qiniu/index.test.ts +294 -1
  136. package/packages/model-runtime/src/providers/qiniu/index.ts +8 -3
  137. package/packages/model-runtime/src/providers/search1api/index.test.ts +1131 -11
  138. package/packages/model-runtime/src/providers/search1api/index.ts +10 -4
  139. package/packages/model-runtime/src/providers/sensenova/index.test.ts +1069 -1
  140. package/packages/model-runtime/src/providers/sensenova/index.ts +8 -3
  141. package/packages/model-runtime/src/providers/siliconcloud/index.test.ts +196 -0
  142. package/packages/model-runtime/src/providers/siliconcloud/index.ts +8 -3
  143. package/packages/model-runtime/src/providers/spark/index.test.ts +293 -1
  144. package/packages/model-runtime/src/providers/spark/index.ts +8 -3
  145. package/packages/model-runtime/src/providers/stepfun/index.test.ts +322 -3
  146. package/packages/model-runtime/src/providers/stepfun/index.ts +8 -3
  147. package/packages/model-runtime/src/providers/tencentcloud/index.test.ts +182 -3
  148. package/packages/model-runtime/src/providers/tencentcloud/index.ts +8 -3
  149. package/packages/model-runtime/src/providers/togetherai/index.test.ts +359 -4
  150. package/packages/model-runtime/src/providers/togetherai/index.ts +12 -5
  151. package/packages/model-runtime/src/providers/v0/index.test.ts +341 -0
  152. package/packages/model-runtime/src/providers/v0/index.ts +20 -6
  153. package/packages/model-runtime/src/providers/vercelaigateway/index.test.ts +710 -0
  154. package/packages/model-runtime/src/providers/vercelaigateway/index.ts +19 -13
  155. package/packages/model-runtime/src/providers/vllm/index.test.ts +45 -1
  156. package/packages/model-runtime/src/providers/volcengine/index.test.ts +75 -0
  157. package/packages/model-runtime/src/providers/wenxin/index.test.ts +144 -1
  158. package/packages/model-runtime/src/providers/wenxin/index.ts +8 -3
  159. package/packages/model-runtime/src/providers/xai/index.test.ts +105 -1
  160. package/packages/model-runtime/src/providers/xinference/index.test.ts +70 -1
  161. package/packages/model-runtime/src/providers/zeroone/index.test.ts +327 -3
  162. package/packages/model-runtime/src/providers/zeroone/index.ts +23 -6
  163. package/packages/model-runtime/src/providers/zhipu/index.test.ts +908 -236
  164. package/packages/model-runtime/src/providers/zhipu/index.ts +8 -3
  165. package/packages/model-runtime/src/types/structureOutput.ts +5 -1
  166. package/packages/model-runtime/vitest.config.mts +7 -1
  167. package/packages/types/src/aiChat.ts +20 -2
  168. package/packages/types/src/serverConfig.ts +7 -1
  169. package/packages/types/src/tool/index.ts +1 -0
  170. package/packages/types/src/tool/tool.ts +33 -0
  171. package/packages/types/src/user/settings/image.ts +3 -0
  172. package/packages/types/src/user/settings/index.ts +3 -0
  173. package/src/app/[variants]/(main)/settings/_layout/SettingsContent.tsx +3 -0
  174. package/src/app/[variants]/(main)/settings/hooks/useCategory.tsx +8 -3
  175. package/src/app/[variants]/(main)/settings/image/index.tsx +74 -0
  176. package/src/components/FormInput/FormSliderWithInput.tsx +40 -0
  177. package/src/components/FormInput/index.ts +1 -0
  178. package/src/envs/image.ts +27 -0
  179. package/src/features/Conversation/Messages/Assistant/index.tsx +1 -1
  180. package/src/features/Conversation/Messages/User/index.tsx +2 -2
  181. package/src/hooks/useFetchAiImageConfig.ts +12 -17
  182. package/src/locales/default/setting.ts +8 -0
  183. package/src/server/globalConfig/index.ts +5 -0
  184. package/src/server/routers/lambda/aiChat.ts +2 -0
  185. package/src/store/global/initialState.ts +1 -0
  186. package/src/store/image/slices/generationConfig/action.test.ts +17 -0
  187. package/src/store/image/slices/generationConfig/action.ts +18 -21
  188. package/src/store/image/slices/generationConfig/initialState.ts +3 -2
  189. package/src/store/user/slices/common/action.ts +1 -0
  190. package/src/store/user/slices/settings/selectors/settings.ts +3 -0
@@ -0,0 +1,381 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import type { PipelineContext } from '../../types';
4
+ import { ProcessorError } from '../../types';
5
+ import { BaseProcessor } from '../BaseProcessor';
6
+
7
+ class TestProcessor extends BaseProcessor {
8
+ readonly name = 'TestProcessor';
9
+
10
+ protected async doProcess(context: PipelineContext): Promise<PipelineContext> {
11
+ return this.cloneContext(context);
12
+ }
13
+ }
14
+
15
+ class AbortingProcessor extends BaseProcessor {
16
+ readonly name = 'AbortingProcessor';
17
+
18
+ protected async doProcess(context: PipelineContext): Promise<PipelineContext> {
19
+ return this.abort(context, 'test abort reason');
20
+ }
21
+ }
22
+
23
+ class InvalidOutputProcessor extends BaseProcessor {
24
+ readonly name = 'InvalidOutputProcessor';
25
+
26
+ protected async doProcess(_context: PipelineContext): Promise<PipelineContext> {
27
+ return {} as PipelineContext;
28
+ }
29
+ }
30
+
31
+ class ErrorThrowingProcessor extends BaseProcessor {
32
+ readonly name = 'ErrorThrowingProcessor';
33
+
34
+ protected async doProcess(_context: PipelineContext): Promise<PipelineContext> {
35
+ throw new Error('test error');
36
+ }
37
+ }
38
+
39
+ class MessageCheckingProcessor extends BaseProcessor {
40
+ readonly name = 'MessageCheckingProcessor';
41
+ public lastEmptyCheck: boolean | null = null;
42
+
43
+ protected async doProcess(context: PipelineContext): Promise<PipelineContext> {
44
+ const cloned = this.cloneContext(context);
45
+
46
+ // Test isEmptyMessage
47
+ if (cloned.messages.length > 0) {
48
+ const content = cloned.messages[0].content;
49
+ this.lastEmptyCheck = this.isEmptyMessage(typeof content === 'string' ? content : undefined);
50
+ }
51
+
52
+ return this.markAsExecuted(cloned);
53
+ }
54
+ }
55
+
56
+ describe('BaseProcessor', () => {
57
+ const createContext = (messages: any[] = []): PipelineContext => ({
58
+ initialState: {
59
+ messages: [],
60
+ model: 'test-model',
61
+ provider: 'test-provider',
62
+ },
63
+ isAborted: false,
64
+ messages,
65
+ metadata: {
66
+ maxTokens: 4000,
67
+ model: 'test-model',
68
+ },
69
+ });
70
+
71
+ describe('constructor', () => {
72
+ it('should initialize with options', () => {
73
+ const processor = new TestProcessor({ debug: true });
74
+ expect(processor.name).toBe('TestProcessor');
75
+ });
76
+
77
+ it('should initialize without options', () => {
78
+ const processor = new TestProcessor();
79
+ expect(processor.name).toBe('TestProcessor');
80
+ });
81
+ });
82
+
83
+ describe('process', () => {
84
+ it('should process valid context', async () => {
85
+ const processor = new TestProcessor();
86
+ const context = createContext([{ content: 'test', role: 'user' }]);
87
+
88
+ const result = await processor.process(context);
89
+
90
+ expect(result.messages).toHaveLength(1);
91
+ expect(result.messages[0].content).toBe('test');
92
+ });
93
+
94
+ it('should validate input context', async () => {
95
+ const processor = new TestProcessor();
96
+ const invalidContext = { messages: 'not an array' } as any;
97
+
98
+ await expect(processor.process(invalidContext)).rejects.toThrow(ProcessorError);
99
+ await expect(processor.process(invalidContext)).rejects.toThrow('无效的上下文');
100
+ });
101
+
102
+ it('should validate output context', async () => {
103
+ const processor = new InvalidOutputProcessor();
104
+ const context = createContext([{ content: 'test', role: 'user' }]);
105
+
106
+ await expect(processor.process(context)).rejects.toThrow(ProcessorError);
107
+ await expect(processor.process(context)).rejects.toThrow('无效的输出上下文');
108
+ });
109
+
110
+ it('should wrap errors in ProcessorError', async () => {
111
+ const processor = new ErrorThrowingProcessor();
112
+ const context = createContext();
113
+
114
+ await expect(processor.process(context)).rejects.toThrow(ProcessorError);
115
+
116
+ try {
117
+ await processor.process(context);
118
+ } catch (error) {
119
+ expect(error).toBeInstanceOf(ProcessorError);
120
+ if (error instanceof ProcessorError) {
121
+ expect(error.processorName).toBe('ErrorThrowingProcessor');
122
+ expect(error.originalError).toBeInstanceOf(Error);
123
+ expect(error.originalError?.message).toBe('test error');
124
+ }
125
+ }
126
+ });
127
+
128
+ it('should handle non-Error throws', async () => {
129
+ class StringThrowingProcessor extends BaseProcessor {
130
+ readonly name = 'StringThrowingProcessor';
131
+
132
+ protected async doProcess(_context: PipelineContext): Promise<PipelineContext> {
133
+ throw 'string error';
134
+ }
135
+ }
136
+
137
+ const processor = new StringThrowingProcessor();
138
+ const context = createContext();
139
+
140
+ await expect(processor.process(context)).rejects.toThrow(ProcessorError);
141
+ });
142
+ });
143
+
144
+ describe('validateInput', () => {
145
+ it('should reject null context', async () => {
146
+ const processor = new TestProcessor();
147
+
148
+ await expect(processor.process(null as any)).rejects.toThrow('无效的上下文');
149
+ });
150
+
151
+ it('should reject undefined context', async () => {
152
+ const processor = new TestProcessor();
153
+
154
+ await expect(processor.process(undefined as any)).rejects.toThrow('无效的上下文');
155
+ });
156
+
157
+ it('should reject context without messages array', async () => {
158
+ const processor = new TestProcessor();
159
+ const invalidContext = {
160
+ initialState: {},
161
+ isAborted: false,
162
+ metadata: {},
163
+ } as any;
164
+
165
+ await expect(processor.process(invalidContext)).rejects.toThrow('无效的上下文');
166
+ });
167
+ });
168
+
169
+ describe('cloneContext', () => {
170
+ it('should create shallow copy of context', async () => {
171
+ const processor = new TestProcessor();
172
+ const context = createContext([
173
+ { content: 'test1', role: 'user' },
174
+ { content: 'test2', role: 'assistant' },
175
+ ]);
176
+
177
+ const result = await processor.process(context);
178
+
179
+ expect(result).not.toBe(context);
180
+ expect(result.messages).not.toBe(context.messages);
181
+ expect(result.metadata).not.toBe(context.metadata);
182
+ expect(result.initialState).toBe(context.initialState); // initialState is readonly, so it's shared
183
+ });
184
+
185
+ it('should preserve all context properties', async () => {
186
+ const processor = new TestProcessor();
187
+ const context = createContext([{ content: 'test', role: 'user' }]);
188
+ context.metadata.customField = 'customValue';
189
+
190
+ const result = await processor.process(context);
191
+
192
+ expect(result.isAborted).toBe(context.isAborted);
193
+ expect(result.metadata.model).toBe(context.metadata.model);
194
+ expect(result.metadata.maxTokens).toBe(context.metadata.maxTokens);
195
+ expect(result.metadata.customField).toBe('customValue');
196
+ });
197
+ });
198
+
199
+ describe('abort', () => {
200
+ it('should set isAborted to true', async () => {
201
+ const processor = new AbortingProcessor();
202
+ const context = createContext();
203
+
204
+ const result = await processor.process(context);
205
+
206
+ expect(result.isAborted).toBe(true);
207
+ });
208
+
209
+ it('should set abort reason', async () => {
210
+ const processor = new AbortingProcessor();
211
+ const context = createContext();
212
+
213
+ const result = await processor.process(context);
214
+
215
+ expect(result.abortReason).toBe('test abort reason');
216
+ });
217
+
218
+ it('should preserve other context properties', async () => {
219
+ const processor = new AbortingProcessor();
220
+ const context = createContext([{ content: 'test', role: 'user' }]);
221
+
222
+ const result = await processor.process(context);
223
+
224
+ expect(result.messages).toEqual(context.messages);
225
+ expect(result.metadata).toEqual(context.metadata);
226
+ });
227
+ });
228
+
229
+ describe('isEmptyMessage', () => {
230
+ it('should return true for empty string', async () => {
231
+ const processor = new MessageCheckingProcessor();
232
+ const context = createContext([{ content: '', role: 'user' }]);
233
+
234
+ await processor.process(context);
235
+
236
+ expect(processor.lastEmptyCheck).toBe(true);
237
+ });
238
+
239
+ it('should return true for whitespace-only string', async () => {
240
+ const processor = new MessageCheckingProcessor();
241
+ const context = createContext([{ content: ' \n\t ', role: 'user' }]);
242
+
243
+ await processor.process(context);
244
+
245
+ expect(processor.lastEmptyCheck).toBe(true);
246
+ });
247
+
248
+ it('should return true for null', async () => {
249
+ const processor = new MessageCheckingProcessor();
250
+ const context = createContext([{ content: null, role: 'user' }]);
251
+
252
+ await processor.process(context);
253
+
254
+ expect(processor.lastEmptyCheck).toBe(true);
255
+ });
256
+
257
+ it('should return true for undefined', async () => {
258
+ const processor = new MessageCheckingProcessor();
259
+ const context = createContext([{ content: undefined, role: 'user' }]);
260
+
261
+ await processor.process(context);
262
+
263
+ expect(processor.lastEmptyCheck).toBe(true);
264
+ });
265
+
266
+ it('should return false for non-empty string', async () => {
267
+ const processor = new MessageCheckingProcessor();
268
+ const context = createContext([{ content: 'hello', role: 'user' }]);
269
+
270
+ await processor.process(context);
271
+
272
+ expect(processor.lastEmptyCheck).toBe(false);
273
+ });
274
+
275
+ it('should return false for string with content after trim', async () => {
276
+ const processor = new MessageCheckingProcessor();
277
+ const context = createContext([{ content: ' hello ', role: 'user' }]);
278
+
279
+ await processor.process(context);
280
+
281
+ expect(processor.lastEmptyCheck).toBe(false);
282
+ });
283
+ });
284
+
285
+ describe('markAsExecuted', () => {
286
+ it('should return context unchanged by default', async () => {
287
+ const processor = new MessageCheckingProcessor();
288
+ const context = createContext([{ content: 'test', role: 'user' }]);
289
+
290
+ const result = await processor.process(context);
291
+
292
+ // Since markAsExecuted is called in doProcess, verify context is returned
293
+ expect(result.messages).toEqual(context.messages);
294
+ });
295
+
296
+ it('should be callable multiple times', async () => {
297
+ class MultiMarkProcessor extends BaseProcessor {
298
+ readonly name = 'MultiMarkProcessor';
299
+
300
+ protected async doProcess(context: PipelineContext): Promise<PipelineContext> {
301
+ let result = this.cloneContext(context);
302
+ result = this.markAsExecuted(result);
303
+ result = this.markAsExecuted(result);
304
+ return result;
305
+ }
306
+ }
307
+
308
+ const processor = new MultiMarkProcessor();
309
+ const context = createContext();
310
+
311
+ const result = await processor.process(context);
312
+
313
+ expect(result).toBeDefined();
314
+ expect(result.messages).toEqual([]);
315
+ });
316
+ });
317
+
318
+ describe('integration scenarios', () => {
319
+ it('should handle complex processing pipeline', async () => {
320
+ class ComplexProcessor extends BaseProcessor {
321
+ readonly name = 'ComplexProcessor';
322
+
323
+ protected async doProcess(context: PipelineContext): Promise<PipelineContext> {
324
+ const cloned = this.cloneContext(context);
325
+
326
+ // Check first message
327
+ if (cloned.messages.length > 0) {
328
+ const firstContent = cloned.messages[0].content;
329
+ if (this.isEmptyMessage(typeof firstContent === 'string' ? firstContent : undefined)) {
330
+ return this.abort(cloned, 'First message is empty');
331
+ }
332
+ }
333
+
334
+ // Process messages
335
+ cloned.messages = cloned.messages.map((msg) => ({
336
+ ...msg,
337
+ content: typeof msg.content === 'string' ? msg.content.toUpperCase() : msg.content,
338
+ }));
339
+
340
+ return this.markAsExecuted(cloned);
341
+ }
342
+ }
343
+
344
+ const processor = new ComplexProcessor();
345
+ const context = createContext([
346
+ { content: 'hello', role: 'user' },
347
+ { content: 'world', role: 'assistant' },
348
+ ]);
349
+
350
+ const result = await processor.process(context);
351
+
352
+ expect(result.isAborted).toBe(false);
353
+ expect(result.messages[0].content).toBe('HELLO');
354
+ expect(result.messages[1].content).toBe('WORLD');
355
+ });
356
+
357
+ it('should abort when condition met', async () => {
358
+ class ConditionalAbortProcessor extends BaseProcessor {
359
+ readonly name = 'ConditionalAbortProcessor';
360
+
361
+ protected async doProcess(context: PipelineContext): Promise<PipelineContext> {
362
+ const cloned = this.cloneContext(context);
363
+
364
+ if (cloned.messages.length === 0) {
365
+ return this.abort(cloned, 'No messages to process');
366
+ }
367
+
368
+ return cloned;
369
+ }
370
+ }
371
+
372
+ const processor = new ConditionalAbortProcessor();
373
+ const context = createContext([]);
374
+
375
+ const result = await processor.process(context);
376
+
377
+ expect(result.isAborted).toBe(true);
378
+ expect(result.abortReason).toBe('No messages to process');
379
+ });
380
+ });
381
+ });