@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,485 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ import { ContextEngine } from '../pipeline';
4
+ import type { ContextProcessor, PipelineContext } from '../types';
5
+ import { PipelineError } from '../types';
6
+
7
+ describe('ContextEngine', () => {
8
+ const createMockProcessor = (name: string, delay = 0): ContextProcessor => ({
9
+ name,
10
+ process: vi.fn(async (context) => {
11
+ if (delay > 0) await new Promise((resolve) => setTimeout(resolve, delay));
12
+ return {
13
+ ...context,
14
+ metadata: {
15
+ ...context.metadata,
16
+ [name]: true,
17
+ },
18
+ };
19
+ }),
20
+ });
21
+
22
+ const createAbortingProcessor = (name: string, reason = 'test abort'): ContextProcessor => ({
23
+ name,
24
+ process: vi.fn(async (context) => ({
25
+ ...context,
26
+ abortReason: reason,
27
+ isAborted: true,
28
+ })),
29
+ });
30
+
31
+ const createErrorProcessor = (name: string, error: Error): ContextProcessor => ({
32
+ name,
33
+ process: vi.fn(async () => {
34
+ throw error;
35
+ }),
36
+ });
37
+
38
+ const createInitialContext = (): {
39
+ initialState: any;
40
+ maxTokens: number;
41
+ messages: any[];
42
+ model: string;
43
+ } => ({
44
+ initialState: {
45
+ messages: [],
46
+ model: 'test-model',
47
+ provider: 'test-provider',
48
+ },
49
+ maxTokens: 4000,
50
+ messages: [{ content: 'test', role: 'user' }],
51
+ model: 'test-model',
52
+ });
53
+
54
+ describe('constructor', () => {
55
+ it('should initialize with pipeline and options', () => {
56
+ const processor1 = createMockProcessor('processor1');
57
+ const processor2 = createMockProcessor('processor2');
58
+
59
+ const engine = new ContextEngine({
60
+ debug: true,
61
+ pipeline: [processor1, processor2],
62
+ });
63
+
64
+ expect(engine.getProcessors()).toHaveLength(2);
65
+ expect(engine.getProcessors()[0]).toBe(processor1);
66
+ expect(engine.getProcessors()[1]).toBe(processor2);
67
+ });
68
+
69
+ it('should initialize with empty pipeline', () => {
70
+ const engine = new ContextEngine({ pipeline: [] });
71
+ expect(engine.getProcessors()).toHaveLength(0);
72
+ });
73
+ });
74
+
75
+ describe('addProcessor', () => {
76
+ it('should add processor to pipeline', () => {
77
+ const engine = new ContextEngine({ pipeline: [] });
78
+ const processor = createMockProcessor('test');
79
+
80
+ engine.addProcessor(processor);
81
+
82
+ expect(engine.getProcessors()).toHaveLength(1);
83
+ expect(engine.getProcessors()[0]).toBe(processor);
84
+ });
85
+
86
+ it('should support chaining', () => {
87
+ const engine = new ContextEngine({ pipeline: [] });
88
+ const processor1 = createMockProcessor('p1');
89
+ const processor2 = createMockProcessor('p2');
90
+
91
+ const result = engine.addProcessor(processor1).addProcessor(processor2);
92
+
93
+ expect(result).toBe(engine);
94
+ expect(engine.getProcessors()).toHaveLength(2);
95
+ });
96
+ });
97
+
98
+ describe('removeProcessor', () => {
99
+ it('should remove processor by name', () => {
100
+ const processor1 = createMockProcessor('p1');
101
+ const processor2 = createMockProcessor('p2');
102
+ const processor3 = createMockProcessor('p3');
103
+
104
+ const engine = new ContextEngine({
105
+ pipeline: [processor1, processor2, processor3],
106
+ });
107
+
108
+ engine.removeProcessor('p2');
109
+
110
+ const processors = engine.getProcessors();
111
+ expect(processors).toHaveLength(2);
112
+ expect(processors[0].name).toBe('p1');
113
+ expect(processors[1].name).toBe('p3');
114
+ });
115
+
116
+ it('should support chaining', () => {
117
+ const processor1 = createMockProcessor('p1');
118
+ const processor2 = createMockProcessor('p2');
119
+
120
+ const engine = new ContextEngine({
121
+ pipeline: [processor1, processor2],
122
+ });
123
+
124
+ const result = engine.removeProcessor('p1').removeProcessor('p2');
125
+
126
+ expect(result).toBe(engine);
127
+ expect(engine.getProcessors()).toHaveLength(0);
128
+ });
129
+
130
+ it('should do nothing if processor not found', () => {
131
+ const processor = createMockProcessor('p1');
132
+ const engine = new ContextEngine({ pipeline: [processor] });
133
+
134
+ engine.removeProcessor('nonexistent');
135
+
136
+ expect(engine.getProcessors()).toHaveLength(1);
137
+ });
138
+ });
139
+
140
+ describe('getProcessors', () => {
141
+ it('should return copy of processor list', () => {
142
+ const processor = createMockProcessor('test');
143
+ const engine = new ContextEngine({ pipeline: [processor] });
144
+
145
+ const processors = engine.getProcessors();
146
+ processors.push(createMockProcessor('new'));
147
+
148
+ expect(engine.getProcessors()).toHaveLength(1);
149
+ });
150
+ });
151
+
152
+ describe('clear', () => {
153
+ it('should remove all processors', () => {
154
+ const engine = new ContextEngine({
155
+ pipeline: [createMockProcessor('p1'), createMockProcessor('p2')],
156
+ });
157
+
158
+ engine.clear();
159
+
160
+ expect(engine.getProcessors()).toHaveLength(0);
161
+ });
162
+
163
+ it('should support chaining', () => {
164
+ const engine = new ContextEngine({
165
+ pipeline: [createMockProcessor('p1')],
166
+ });
167
+
168
+ const result = engine.clear();
169
+
170
+ expect(result).toBe(engine);
171
+ });
172
+ });
173
+
174
+ describe('process', () => {
175
+ it('should execute processors in sequence', async () => {
176
+ const processor1 = createMockProcessor('p1');
177
+ const processor2 = createMockProcessor('p2');
178
+ const processor3 = createMockProcessor('p3');
179
+
180
+ const engine = new ContextEngine({
181
+ pipeline: [processor1, processor2, processor3],
182
+ });
183
+
184
+ const input = createInitialContext();
185
+ const result = await engine.process(input);
186
+
187
+ expect(result.isAborted).toBe(false);
188
+ expect(result.messages).toEqual(input.messages);
189
+ expect(result.metadata.p1).toBe(true);
190
+ expect(result.metadata.p2).toBe(true);
191
+ expect(result.metadata.p3).toBe(true);
192
+ expect(result.stats.processedCount).toBe(3);
193
+ });
194
+
195
+ it('should handle messages array correctly', async () => {
196
+ const processor = createMockProcessor('p1');
197
+ const engine = new ContextEngine({ pipeline: [processor] });
198
+
199
+ const input = createInitialContext();
200
+ const result = await engine.process(input);
201
+
202
+ expect(result.messages).toEqual(input.messages);
203
+ });
204
+
205
+ it('should handle empty messages', async () => {
206
+ const processor = createMockProcessor('p1');
207
+ const engine = new ContextEngine({ pipeline: [processor] });
208
+
209
+ const input = { ...createInitialContext(), messages: undefined };
210
+ const result = await engine.process(input);
211
+
212
+ expect(result.messages).toEqual([]);
213
+ });
214
+
215
+ it('should include metadata in context', async () => {
216
+ const processor: ContextProcessor = {
217
+ name: 'test',
218
+ process: vi.fn(async (context) => {
219
+ expect(context.metadata.maxTokens).toBe(4000);
220
+ expect(context.metadata.model).toBe('test-model');
221
+ expect(context.metadata.customKey).toBe('customValue');
222
+ return context;
223
+ }),
224
+ };
225
+
226
+ const engine = new ContextEngine({ pipeline: [processor] });
227
+
228
+ await engine.process({
229
+ ...createInitialContext(),
230
+ metadata: { customKey: 'customValue' },
231
+ });
232
+ });
233
+
234
+ it('should track execution stats', async () => {
235
+ const processor1 = createMockProcessor('p1', 10);
236
+ const processor2 = createMockProcessor('p2', 20);
237
+
238
+ const engine = new ContextEngine({
239
+ pipeline: [processor1, processor2],
240
+ });
241
+
242
+ const result = await engine.process(createInitialContext());
243
+
244
+ expect(result.stats.processedCount).toBe(2);
245
+ expect(result.stats.totalDuration).toBeGreaterThanOrEqual(20);
246
+ expect(result.stats.processorDurations.p1).toBeGreaterThanOrEqual(10);
247
+ expect(result.stats.processorDurations.p2).toBeGreaterThanOrEqual(20);
248
+ });
249
+
250
+ it('should stop processing when aborted', async () => {
251
+ const processor1 = createMockProcessor('p1');
252
+ const processor2 = createAbortingProcessor('p2', 'user requested');
253
+ const processor3 = createMockProcessor('p3');
254
+
255
+ const engine = new ContextEngine({
256
+ pipeline: [processor1, processor2, processor3],
257
+ });
258
+
259
+ const result = await engine.process(createInitialContext());
260
+
261
+ expect(result.isAborted).toBe(true);
262
+ expect(result.abortReason).toBe('user requested');
263
+ expect(result.stats.processedCount).toBe(2);
264
+ expect(processor1.process).toHaveBeenCalled();
265
+ expect(processor2.process).toHaveBeenCalled();
266
+ expect(processor3.process).not.toHaveBeenCalled();
267
+ });
268
+
269
+ it('should skip remaining processors if context is already aborted', async () => {
270
+ const processor1: ContextProcessor = {
271
+ name: 'p1',
272
+ process: vi.fn(async (context) => ({
273
+ ...context,
274
+ isAborted: true,
275
+ })),
276
+ };
277
+ const processor2 = createMockProcessor('p2');
278
+
279
+ const engine = new ContextEngine({
280
+ pipeline: [processor1, processor2],
281
+ });
282
+
283
+ const input = {
284
+ ...createInitialContext(),
285
+ };
286
+ input.initialState = { ...input.initialState, messages: [] };
287
+
288
+ const result = await engine.process(input);
289
+
290
+ expect(result.isAborted).toBe(true);
291
+ expect(result.stats.processedCount).toBe(1);
292
+ expect(processor2.process).not.toHaveBeenCalled();
293
+ });
294
+
295
+ it('should throw PipelineError when processor fails', async () => {
296
+ const error = new Error('processor failed');
297
+ const processor = createErrorProcessor('failing-processor', error);
298
+
299
+ const engine = new ContextEngine({ pipeline: [processor] });
300
+
301
+ await expect(engine.process(createInitialContext())).rejects.toThrow(PipelineError);
302
+ await expect(engine.process(createInitialContext())).rejects.toThrow(
303
+ 'Processor [failing-processor] execution failed',
304
+ );
305
+ });
306
+
307
+ it('should include processor stats even when it fails', async () => {
308
+ const error = new Error('test error');
309
+ const processor = createErrorProcessor('failing', error);
310
+
311
+ const engine = new ContextEngine({ pipeline: [processor] });
312
+
313
+ try {
314
+ await engine.process(createInitialContext());
315
+ } catch (e) {
316
+ expect(e).toBeInstanceOf(PipelineError);
317
+ if (e instanceof PipelineError) {
318
+ expect(e.processorName).toBe('failing');
319
+ expect(e.originalError).toBe(error);
320
+ }
321
+ }
322
+ });
323
+
324
+ it('should handle non-Error objects thrown by processor', async () => {
325
+ const processor: ContextProcessor = {
326
+ name: 'thrower',
327
+ process: vi.fn(async () => {
328
+ throw 'string error';
329
+ }),
330
+ };
331
+
332
+ const engine = new ContextEngine({ pipeline: [processor] });
333
+
334
+ await expect(engine.process(createInitialContext())).rejects.toThrow(PipelineError);
335
+ });
336
+
337
+ it('should preserve initial state', async () => {
338
+ const processor: ContextProcessor = {
339
+ name: 'test',
340
+ process: vi.fn(async (context) => {
341
+ expect(context.initialState).toEqual(createInitialContext().initialState);
342
+ return context;
343
+ }),
344
+ };
345
+
346
+ const engine = new ContextEngine({ pipeline: [processor] });
347
+ await engine.process(createInitialContext());
348
+ });
349
+ });
350
+
351
+ describe('getStats', () => {
352
+ it('should return processor count and names', () => {
353
+ const processor1 = createMockProcessor('p1');
354
+ const processor2 = createMockProcessor('p2');
355
+
356
+ const engine = new ContextEngine({
357
+ pipeline: [processor1, processor2],
358
+ });
359
+
360
+ const stats = engine.getStats();
361
+
362
+ expect(stats.processorCount).toBe(2);
363
+ expect(stats.processorNames).toEqual(['p1', 'p2']);
364
+ });
365
+
366
+ it('should return empty stats for empty pipeline', () => {
367
+ const engine = new ContextEngine({ pipeline: [] });
368
+
369
+ const stats = engine.getStats();
370
+
371
+ expect(stats.processorCount).toBe(0);
372
+ expect(stats.processorNames).toEqual([]);
373
+ });
374
+ });
375
+
376
+ describe('clone', () => {
377
+ it('should create independent copy of engine', () => {
378
+ const processor1 = createMockProcessor('p1');
379
+ const processor2 = createMockProcessor('p2');
380
+
381
+ const engine1 = new ContextEngine({
382
+ debug: true,
383
+ pipeline: [processor1, processor2],
384
+ });
385
+
386
+ const engine2 = engine1.clone();
387
+
388
+ expect(engine2).not.toBe(engine1);
389
+ expect(engine2.getProcessors()).toHaveLength(2);
390
+ expect(engine2.getProcessors()[0]).toBe(processor1);
391
+ expect(engine2.getProcessors()[1]).toBe(processor2);
392
+
393
+ // Modify cloned engine
394
+ engine2.addProcessor(createMockProcessor('p3'));
395
+
396
+ // Original should be unchanged
397
+ expect(engine1.getProcessors()).toHaveLength(2);
398
+ expect(engine2.getProcessors()).toHaveLength(3);
399
+ });
400
+ });
401
+
402
+ describe('validate', () => {
403
+ it('should return valid for correct pipeline', () => {
404
+ const processor1 = createMockProcessor('p1');
405
+ const processor2 = createMockProcessor('p2');
406
+
407
+ const engine = new ContextEngine({
408
+ pipeline: [processor1, processor2],
409
+ });
410
+
411
+ const result = engine.validate();
412
+
413
+ expect(result.valid).toBe(true);
414
+ expect(result.errors).toHaveLength(0);
415
+ });
416
+
417
+ it('should detect duplicate processor names', () => {
418
+ const processor1 = createMockProcessor('duplicate');
419
+ const processor2 = createMockProcessor('duplicate');
420
+
421
+ const engine = new ContextEngine({
422
+ pipeline: [processor1, processor2],
423
+ });
424
+
425
+ const result = engine.validate();
426
+
427
+ expect(result.valid).toBe(false);
428
+ expect(result.errors).toContain('Found duplicate processor names: duplicate');
429
+ });
430
+
431
+ it('should detect empty pipeline', () => {
432
+ const engine = new ContextEngine({ pipeline: [] });
433
+
434
+ const result = engine.validate();
435
+
436
+ expect(result.valid).toBe(false);
437
+ expect(result.errors).toContain('No processors in pipeline');
438
+ });
439
+
440
+ it('should detect missing processor name', () => {
441
+ const processor: ContextProcessor = {
442
+ name: '',
443
+ process: vi.fn(),
444
+ };
445
+
446
+ const engine = new ContextEngine({ pipeline: [processor] });
447
+
448
+ const result = engine.validate();
449
+
450
+ expect(result.valid).toBe(false);
451
+ expect(result.errors).toContain('Processor missing name');
452
+ });
453
+
454
+ it('should detect missing process method', () => {
455
+ const processor = {
456
+ name: 'test',
457
+ } as any;
458
+
459
+ const engine = new ContextEngine({ pipeline: [processor] });
460
+
461
+ const result = engine.validate();
462
+
463
+ expect(result.valid).toBe(false);
464
+ expect(result.errors).toContain('Processor [test] missing process method');
465
+ });
466
+
467
+ it('should detect multiple errors', () => {
468
+ const processor1 = createMockProcessor('duplicate');
469
+ const processor2 = createMockProcessor('duplicate');
470
+ const processor3: ContextProcessor = {
471
+ name: '',
472
+ process: vi.fn(),
473
+ };
474
+
475
+ const engine = new ContextEngine({
476
+ pipeline: [processor1, processor2, processor3],
477
+ });
478
+
479
+ const result = engine.validate();
480
+
481
+ expect(result.valid).toBe(false);
482
+ expect(result.errors.length).toBeGreaterThan(1);
483
+ });
484
+ });
485
+ });