illuma-agents 1.0.8 → 1.0.9

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 (217) hide show
  1. package/LICENSE +1 -5
  2. package/dist/cjs/common/enum.cjs +1 -2
  3. package/dist/cjs/common/enum.cjs.map +1 -1
  4. package/dist/cjs/instrumentation.cjs.map +1 -1
  5. package/dist/cjs/llm/anthropic/types.cjs.map +1 -1
  6. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +79 -2
  7. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  8. package/dist/cjs/llm/anthropic/utils/tools.cjs.map +1 -1
  9. package/dist/cjs/llm/bedrock/index.cjs +99 -0
  10. package/dist/cjs/llm/bedrock/index.cjs.map +1 -0
  11. package/dist/cjs/llm/fake.cjs.map +1 -1
  12. package/dist/cjs/llm/providers.cjs +13 -16
  13. package/dist/cjs/llm/providers.cjs.map +1 -1
  14. package/dist/cjs/llm/text.cjs.map +1 -1
  15. package/dist/cjs/messages/core.cjs +14 -14
  16. package/dist/cjs/messages/core.cjs.map +1 -1
  17. package/dist/cjs/messages/ids.cjs.map +1 -1
  18. package/dist/cjs/messages/prune.cjs.map +1 -1
  19. package/dist/cjs/run.cjs +10 -1
  20. package/dist/cjs/run.cjs.map +1 -1
  21. package/dist/cjs/splitStream.cjs.map +1 -1
  22. package/dist/cjs/stream.cjs +4 -1
  23. package/dist/cjs/stream.cjs.map +1 -1
  24. package/dist/cjs/tools/ToolNode.cjs +10 -1
  25. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  26. package/dist/cjs/tools/handlers.cjs +29 -25
  27. package/dist/cjs/tools/handlers.cjs.map +1 -1
  28. package/dist/cjs/tools/search/anthropic.cjs.map +1 -1
  29. package/dist/cjs/tools/search/content.cjs.map +1 -1
  30. package/dist/cjs/tools/search/firecrawl.cjs.map +1 -1
  31. package/dist/cjs/tools/search/format.cjs.map +1 -1
  32. package/dist/cjs/tools/search/highlights.cjs.map +1 -1
  33. package/dist/cjs/tools/search/rerankers.cjs.map +1 -1
  34. package/dist/cjs/tools/search/schema.cjs +25 -25
  35. package/dist/cjs/tools/search/schema.cjs.map +1 -1
  36. package/dist/cjs/tools/search/search.cjs +6 -1
  37. package/dist/cjs/tools/search/search.cjs.map +1 -1
  38. package/dist/cjs/tools/search/serper-scraper.cjs.map +1 -1
  39. package/dist/cjs/tools/search/tool.cjs +162 -35
  40. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  41. package/dist/cjs/tools/search/utils.cjs.map +1 -1
  42. package/dist/cjs/utils/graph.cjs.map +1 -1
  43. package/dist/cjs/utils/llm.cjs +0 -1
  44. package/dist/cjs/utils/llm.cjs.map +1 -1
  45. package/dist/cjs/utils/misc.cjs.map +1 -1
  46. package/dist/cjs/utils/run.cjs.map +1 -1
  47. package/dist/cjs/utils/title.cjs +7 -7
  48. package/dist/cjs/utils/title.cjs.map +1 -1
  49. package/dist/esm/common/enum.mjs +1 -2
  50. package/dist/esm/common/enum.mjs.map +1 -1
  51. package/dist/esm/instrumentation.mjs.map +1 -1
  52. package/dist/esm/llm/anthropic/types.mjs.map +1 -1
  53. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +79 -2
  54. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  55. package/dist/esm/llm/anthropic/utils/tools.mjs.map +1 -1
  56. package/dist/esm/llm/bedrock/index.mjs +97 -0
  57. package/dist/esm/llm/bedrock/index.mjs.map +1 -0
  58. package/dist/esm/llm/fake.mjs.map +1 -1
  59. package/dist/esm/llm/providers.mjs +2 -5
  60. package/dist/esm/llm/providers.mjs.map +1 -1
  61. package/dist/esm/llm/text.mjs.map +1 -1
  62. package/dist/esm/messages/core.mjs +14 -14
  63. package/dist/esm/messages/core.mjs.map +1 -1
  64. package/dist/esm/messages/ids.mjs.map +1 -1
  65. package/dist/esm/messages/prune.mjs.map +1 -1
  66. package/dist/esm/run.mjs +10 -1
  67. package/dist/esm/run.mjs.map +1 -1
  68. package/dist/esm/splitStream.mjs.map +1 -1
  69. package/dist/esm/stream.mjs +4 -1
  70. package/dist/esm/stream.mjs.map +1 -1
  71. package/dist/esm/tools/ToolNode.mjs +10 -1
  72. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  73. package/dist/esm/tools/handlers.mjs +30 -26
  74. package/dist/esm/tools/handlers.mjs.map +1 -1
  75. package/dist/esm/tools/search/anthropic.mjs.map +1 -1
  76. package/dist/esm/tools/search/content.mjs.map +1 -1
  77. package/dist/esm/tools/search/firecrawl.mjs.map +1 -1
  78. package/dist/esm/tools/search/format.mjs.map +1 -1
  79. package/dist/esm/tools/search/highlights.mjs.map +1 -1
  80. package/dist/esm/tools/search/rerankers.mjs.map +1 -1
  81. package/dist/esm/tools/search/schema.mjs +25 -25
  82. package/dist/esm/tools/search/schema.mjs.map +1 -1
  83. package/dist/esm/tools/search/search.mjs +6 -1
  84. package/dist/esm/tools/search/search.mjs.map +1 -1
  85. package/dist/esm/tools/search/serper-scraper.mjs.map +1 -1
  86. package/dist/esm/tools/search/tool.mjs +162 -35
  87. package/dist/esm/tools/search/tool.mjs.map +1 -1
  88. package/dist/esm/tools/search/utils.mjs.map +1 -1
  89. package/dist/esm/utils/graph.mjs.map +1 -1
  90. package/dist/esm/utils/llm.mjs +0 -1
  91. package/dist/esm/utils/llm.mjs.map +1 -1
  92. package/dist/esm/utils/misc.mjs.map +1 -1
  93. package/dist/esm/utils/run.mjs.map +1 -1
  94. package/dist/esm/utils/title.mjs +7 -7
  95. package/dist/esm/utils/title.mjs.map +1 -1
  96. package/dist/types/common/enum.d.ts +1 -2
  97. package/dist/types/llm/bedrock/index.d.ts +36 -0
  98. package/dist/types/tools/search/types.d.ts +2 -0
  99. package/dist/types/types/llm.d.ts +3 -8
  100. package/package.json +15 -11
  101. package/src/common/enum.ts +1 -2
  102. package/src/common/index.ts +1 -1
  103. package/src/instrumentation.ts +22 -22
  104. package/src/llm/anthropic/llm.spec.ts +1442 -1442
  105. package/src/llm/anthropic/types.ts +140 -140
  106. package/src/llm/anthropic/utils/message_inputs.ts +757 -660
  107. package/src/llm/anthropic/utils/output_parsers.ts +133 -133
  108. package/src/llm/anthropic/utils/tools.ts +29 -29
  109. package/src/llm/bedrock/index.ts +128 -0
  110. package/src/llm/fake.ts +133 -133
  111. package/src/llm/google/utils/tools.ts +160 -160
  112. package/src/llm/openai/types.ts +24 -24
  113. package/src/llm/openai/utils/isReasoningModel.test.ts +90 -90
  114. package/src/llm/providers.ts +2 -7
  115. package/src/llm/text.ts +94 -94
  116. package/src/messages/core.ts +463 -463
  117. package/src/messages/formatAgentMessages.tools.test.ts +400 -400
  118. package/src/messages/formatMessage.test.ts +693 -693
  119. package/src/messages/ids.ts +26 -26
  120. package/src/messages/prune.ts +567 -567
  121. package/src/messages/shiftIndexTokenCountMap.test.ts +81 -81
  122. package/src/mockStream.ts +98 -98
  123. package/src/prompts/collab.ts +5 -5
  124. package/src/prompts/index.ts +1 -1
  125. package/src/prompts/taskmanager.ts +61 -61
  126. package/src/run.ts +13 -4
  127. package/src/scripts/ant_web_search_edge_case.ts +162 -0
  128. package/src/scripts/ant_web_search_error_edge_case.ts +148 -0
  129. package/src/scripts/args.ts +48 -48
  130. package/src/scripts/caching.ts +123 -123
  131. package/src/scripts/code_exec_files.ts +193 -193
  132. package/src/scripts/empty_input.ts +137 -137
  133. package/src/scripts/image.ts +178 -178
  134. package/src/scripts/memory.ts +97 -97
  135. package/src/scripts/thinking.ts +149 -149
  136. package/src/specs/anthropic.simple.test.ts +67 -0
  137. package/src/specs/spec.utils.ts +3 -3
  138. package/src/specs/token-distribution-edge-case.test.ts +316 -316
  139. package/src/specs/tool-error.test.ts +193 -193
  140. package/src/splitStream.test.ts +691 -691
  141. package/src/splitStream.ts +234 -234
  142. package/src/stream.test.ts +94 -94
  143. package/src/stream.ts +4 -1
  144. package/src/tools/ToolNode.ts +12 -1
  145. package/src/tools/handlers.ts +32 -28
  146. package/src/tools/search/anthropic.ts +51 -51
  147. package/src/tools/search/content.test.ts +173 -173
  148. package/src/tools/search/content.ts +147 -147
  149. package/src/tools/search/direct-url.test.ts +530 -0
  150. package/src/tools/search/firecrawl.ts +210 -210
  151. package/src/tools/search/format.ts +250 -250
  152. package/src/tools/search/highlights.ts +320 -320
  153. package/src/tools/search/index.ts +2 -2
  154. package/src/tools/search/jina-reranker.test.ts +126 -126
  155. package/src/tools/search/output.md +2775 -2775
  156. package/src/tools/search/rerankers.ts +242 -242
  157. package/src/tools/search/schema.ts +63 -63
  158. package/src/tools/search/search.ts +766 -759
  159. package/src/tools/search/serper-scraper.ts +155 -155
  160. package/src/tools/search/test.html +883 -883
  161. package/src/tools/search/test.md +642 -642
  162. package/src/tools/search/test.ts +159 -159
  163. package/src/tools/search/tool.ts +619 -471
  164. package/src/tools/search/types.ts +689 -687
  165. package/src/tools/search/utils.ts +79 -79
  166. package/src/types/index.ts +6 -6
  167. package/src/types/llm.ts +2 -8
  168. package/src/utils/graph.ts +10 -10
  169. package/src/utils/llm.ts +26 -27
  170. package/src/utils/llmConfig.ts +5 -3
  171. package/src/utils/logging.ts +48 -48
  172. package/src/utils/misc.ts +57 -57
  173. package/src/utils/run.ts +100 -100
  174. package/src/utils/title.ts +165 -165
  175. package/dist/cjs/llm/ollama/index.cjs +0 -70
  176. package/dist/cjs/llm/ollama/index.cjs.map +0 -1
  177. package/dist/cjs/llm/ollama/utils.cjs +0 -158
  178. package/dist/cjs/llm/ollama/utils.cjs.map +0 -1
  179. package/dist/esm/llm/ollama/index.mjs +0 -68
  180. package/dist/esm/llm/ollama/index.mjs.map +0 -1
  181. package/dist/esm/llm/ollama/utils.mjs +0 -155
  182. package/dist/esm/llm/ollama/utils.mjs.map +0 -1
  183. package/dist/types/llm/ollama/index.d.ts +0 -8
  184. package/dist/types/llm/ollama/utils.d.ts +0 -7
  185. package/src/llm/ollama/index.ts +0 -92
  186. package/src/llm/ollama/utils.ts +0 -193
  187. package/src/proto/CollabGraph.ts +0 -269
  188. package/src/proto/TaskManager.ts +0 -243
  189. package/src/proto/collab.ts +0 -200
  190. package/src/proto/collab_design.ts +0 -184
  191. package/src/proto/collab_design_v2.ts +0 -224
  192. package/src/proto/collab_design_v3.ts +0 -255
  193. package/src/proto/collab_design_v4.ts +0 -220
  194. package/src/proto/collab_design_v5.ts +0 -251
  195. package/src/proto/collab_graph.ts +0 -181
  196. package/src/proto/collab_original.ts +0 -123
  197. package/src/proto/example.ts +0 -93
  198. package/src/proto/example_new.ts +0 -68
  199. package/src/proto/example_old.ts +0 -201
  200. package/src/proto/example_test.ts +0 -152
  201. package/src/proto/example_test_anthropic.ts +0 -100
  202. package/src/proto/log_stream.ts +0 -202
  203. package/src/proto/main_collab_community_event.ts +0 -133
  204. package/src/proto/main_collab_design_v2.ts +0 -96
  205. package/src/proto/main_collab_design_v4.ts +0 -100
  206. package/src/proto/main_collab_design_v5.ts +0 -135
  207. package/src/proto/main_collab_global_analysis.ts +0 -122
  208. package/src/proto/main_collab_hackathon_event.ts +0 -153
  209. package/src/proto/main_collab_space_mission.ts +0 -153
  210. package/src/proto/main_philosophy.ts +0 -210
  211. package/src/proto/original_script.ts +0 -126
  212. package/src/proto/standard.ts +0 -100
  213. package/src/proto/stream.ts +0 -56
  214. package/src/proto/tasks.ts +0 -118
  215. package/src/proto/tools/global_analysis_tools.ts +0 -86
  216. package/src/proto/tools/space_mission_tools.ts +0 -60
  217. package/src/proto/vertexai.ts +0 -54
@@ -201,7 +201,18 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
201
201
 
202
202
  outputs = await Promise.all(
203
203
  aiMessage.tool_calls
204
- ?.filter((call) => call.id == null || !toolMessageIds.has(call.id))
204
+ ?.filter((call) => {
205
+ /**
206
+ * Filter out:
207
+ * 1. Already processed tool calls (present in toolMessageIds)
208
+ * 2. Server tool calls (e.g., web_search with IDs starting with 'srvtoolu_')
209
+ * which are executed by the provider's API and don't require invocation
210
+ */
211
+ return (
212
+ (call.id == null || !toolMessageIds.has(call.id)) &&
213
+ !(call.id?.startsWith('srvtoolu_') ?? false)
214
+ );
215
+ })
205
216
  .map((call) => this.runTool(call, config)) ?? []
206
217
  );
207
218
  }
@@ -9,7 +9,6 @@ import type { AgentContext } from '@/agents/AgentContext';
9
9
  import type * as t from '@/types';
10
10
  import {
11
11
  ToolCallTypes,
12
- ContentTypes,
13
12
  GraphEvents,
14
13
  StepTypes,
15
14
  Providers,
@@ -87,22 +86,33 @@ export async function handleToolCallChunks({
87
86
  const alreadyDispatched =
88
87
  prevRunStep?.type === StepTypes.MESSAGE_CREATION &&
89
88
  graph.messageStepHasToolCalls.has(prevStepId);
90
- if (!alreadyDispatched && tool_calls?.length === toolCallChunks.length) {
91
- await graph.dispatchMessageDelta(prevStepId, {
92
- content: [
93
- {
94
- type: ContentTypes.TEXT,
95
- text: '',
96
- tool_call_ids: tool_calls.map((tc) => tc.id ?? ''),
97
- },
98
- ],
99
- });
89
+
90
+ if (prevRunStep?.type === StepTypes.TOOL_CALLS) {
91
+ /**
92
+ * If previous step is already a tool_calls step, use that step ID
93
+ * This ensures tool call deltas are dispatched to the correct step
94
+ */
95
+ stepId = prevStepId;
96
+ } else if (
97
+ !alreadyDispatched &&
98
+ prevRunStep?.type === StepTypes.MESSAGE_CREATION
99
+ ) {
100
+ /**
101
+ * Create tool_calls step as soon as we receive the first tool call chunk
102
+ * This ensures deltas are always associated with the correct step
103
+ *
104
+ * NOTE: We do NOT dispatch an empty text block here because:
105
+ * - Empty text blocks cause providers (Anthropic, Bedrock) to reject messages
106
+ * - The tool_calls themselves are sufficient for the step
107
+ * - Empty content with tool_call_ids gets stored in conversation history
108
+ * and causes "messages must have non-empty content" errors on replay
109
+ */
100
110
  graph.messageStepHasToolCalls.set(prevStepId, true);
101
111
  stepId = await graph.dispatchRunStep(
102
112
  stepKey,
103
113
  {
104
114
  type: StepTypes.TOOL_CALLS,
105
- tool_calls,
115
+ tool_calls: tool_calls ?? [],
106
116
  },
107
117
  metadata
108
118
  );
@@ -149,26 +159,21 @@ export const handleToolCalls = async (
149
159
  // no previous step
150
160
  }
151
161
 
152
- const dispatchToolCallIds = async (
153
- lastMessageStepId: string
154
- ): Promise<void> => {
155
- await graph.dispatchMessageDelta(lastMessageStepId, {
156
- content: [
157
- {
158
- type: 'text',
159
- text: '',
160
- tool_call_ids: [toolCallId],
161
- },
162
- ],
163
- });
164
- };
162
+ /**
163
+ * NOTE: We do NOT dispatch empty text blocks with tool_call_ids because:
164
+ * - Empty text blocks cause providers (Anthropic, Bedrock) to reject messages
165
+ * - They get stored in conversation history and cause errors on replay:
166
+ * "messages must have non-empty content" (Anthropic)
167
+ * "The content field in the Message object is empty" (Bedrock)
168
+ * - The tool_calls themselves are sufficient
169
+ */
170
+
165
171
  /* If the previous step exists and is a message creation */
166
172
  if (
167
173
  prevStepId &&
168
174
  prevRunStep &&
169
175
  prevRunStep.type === StepTypes.MESSAGE_CREATION
170
176
  ) {
171
- await dispatchToolCallIds(prevStepId);
172
177
  graph.messageStepHasToolCalls.set(prevStepId, true);
173
178
  /* If the previous step doesn't exist or is not a message creation */
174
179
  } else if (
@@ -186,8 +191,7 @@ export const handleToolCalls = async (
186
191
  },
187
192
  metadata
188
193
  );
189
- await dispatchToolCallIds(stepId);
190
- graph.messageStepHasToolCalls.set(prevStepId, true);
194
+ graph.messageStepHasToolCalls.set(stepId, true);
191
195
  }
192
196
 
193
197
  await graph.dispatchRunStep(
@@ -1,51 +1,51 @@
1
- import type {
2
- AnthropicTextBlockParam,
3
- AnthropicWebSearchResultBlockParam,
4
- } from '@/llm/anthropic/types';
5
- import type { SearchResultData, ProcessedOrganic } from './types';
6
- import { getAttribution } from './utils';
7
-
8
- /**
9
- * Coerces Anthropic web search results to the SearchResultData format
10
- * @param results - Array of Anthropic web search results
11
- * @param turn - The turn number to associate with these results
12
- * @returns SearchResultData with minimal ProcessedOrganic items
13
- */
14
- export function coerceAnthropicSearchResults({
15
- results,
16
- turn = 0,
17
- }: {
18
- results: (AnthropicTextBlockParam | AnthropicWebSearchResultBlockParam)[];
19
- turn?: number;
20
- }): SearchResultData {
21
- const organic: ProcessedOrganic[] = results
22
- .filter((result) => result.type === 'web_search_result')
23
- .map((result, index) => ({
24
- link: result.url,
25
- position: index + 1,
26
- title: result.title,
27
- date: result.page_age ?? undefined,
28
- attribution: getAttribution(result.url),
29
- }));
30
-
31
- return {
32
- turn,
33
- organic,
34
- };
35
- }
36
-
37
- /**
38
- * Helper function to check if an object is an Anthropic web search result
39
- */
40
- export function isAnthropicWebSearchResult(
41
- obj: unknown
42
- ): obj is AnthropicWebSearchResultBlockParam {
43
- return (
44
- typeof obj === 'object' &&
45
- obj !== null &&
46
- 'type' in obj &&
47
- obj.type === 'web_search_result' &&
48
- 'url' in obj &&
49
- typeof (obj as Record<string, unknown>).url === 'string'
50
- );
51
- }
1
+ import type {
2
+ AnthropicTextBlockParam,
3
+ AnthropicWebSearchResultBlockParam,
4
+ } from '@/llm/anthropic/types';
5
+ import type { SearchResultData, ProcessedOrganic } from './types';
6
+ import { getAttribution } from './utils';
7
+
8
+ /**
9
+ * Coerces Anthropic web search results to the SearchResultData format
10
+ * @param results - Array of Anthropic web search results
11
+ * @param turn - The turn number to associate with these results
12
+ * @returns SearchResultData with minimal ProcessedOrganic items
13
+ */
14
+ export function coerceAnthropicSearchResults({
15
+ results,
16
+ turn = 0,
17
+ }: {
18
+ results: (AnthropicTextBlockParam | AnthropicWebSearchResultBlockParam)[];
19
+ turn?: number;
20
+ }): SearchResultData {
21
+ const organic: ProcessedOrganic[] = results
22
+ .filter((result) => result.type === 'web_search_result')
23
+ .map((result, index) => ({
24
+ link: result.url,
25
+ position: index + 1,
26
+ title: result.title,
27
+ date: result.page_age ?? undefined,
28
+ attribution: getAttribution(result.url),
29
+ }));
30
+
31
+ return {
32
+ turn,
33
+ organic,
34
+ };
35
+ }
36
+
37
+ /**
38
+ * Helper function to check if an object is an Anthropic web search result
39
+ */
40
+ export function isAnthropicWebSearchResult(
41
+ obj: unknown
42
+ ): obj is AnthropicWebSearchResultBlockParam {
43
+ return (
44
+ typeof obj === 'object' &&
45
+ obj !== null &&
46
+ 'type' in obj &&
47
+ obj.type === 'web_search_result' &&
48
+ 'url' in obj &&
49
+ typeof (obj as Record<string, unknown>).url === 'string'
50
+ );
51
+ }
@@ -1,173 +1,173 @@
1
- /* eslint-disable @typescript-eslint/no-unused-vars */
2
- /* eslint-disable no-console */
3
- // content.test.ts
4
- import * as fs from 'fs';
5
- import { processContent } from './content';
6
-
7
- describe('Link Processor', () => {
8
- afterAll(() => {
9
- if (fs.existsSync('./temp.html')) {
10
- fs.unlinkSync('./temp.html');
11
- }
12
- if (fs.existsSync('./temp.md')) {
13
- fs.unlinkSync('./temp.md');
14
- }
15
- });
16
- // Basic functionality tests
17
- test('should replace basic links with references', () => {
18
- const html = `
19
- <p>Test with <a href="https://example.com/link" title="Example">a link</a></p>
20
- <p>And an <img src="https://example.com/img.jpg" alt="image"></p>
21
- <p>Plus a <video src="https://example.com/video.mp4"></video></p>
22
- `;
23
-
24
- const markdown = `
25
- Test with [a link](https://example.com/link "Example")
26
- And an ![image](https://example.com/img.jpg)
27
- Plus a [video](https://example.com/video.mp4)
28
- `;
29
-
30
- const result = processContent(html, markdown);
31
-
32
- expect(result.links.length).toBe(1);
33
- expect(result.images.length).toBe(1);
34
- expect(result.videos.length).toBe(1);
35
- expect(result.markdown).toContain('link#1');
36
- expect(result.markdown).toContain('image#1');
37
- expect(result.markdown).toContain('video#1');
38
- });
39
-
40
- // Edge case tests
41
- test('should handle links with parentheses and special characters', () => {
42
- const html = `
43
- <a href="https://example.com/page(1).html" title="Parens">Link with parens</a>
44
- <a href="https://example.com/path?query=test&param=value">Link with query</a>
45
- `;
46
-
47
- const markdown = `
48
- [Link with parens](https://example.com/page(1).html "Parens")
49
- [Link with query](https://example.com/path?query=test&param=value)
50
- `;
51
-
52
- const result = processContent(html, markdown);
53
-
54
- expect(result.links.length).toBe(2);
55
- expect(result.markdown).toContain('link#1');
56
- expect(result.markdown).toContain('link#2');
57
- });
58
-
59
- // Performance test with large files
60
- test('should process large files efficiently', () => {
61
- const html = fs.readFileSync('src/tools/search/test.html', 'utf-8');
62
- const markdown = fs.readFileSync('src/tools/search/test.md', 'utf-8');
63
-
64
- // const largeHtml = generateLargeHtml(1000); // 1000 links
65
- // fs.writeFileSync('./temp.html', largeHtml);
66
-
67
- // const largeMd = generateLargeMarkdown(1000); // 1000 links
68
- // fs.writeFileSync('./temp.md', largeMd);
69
-
70
- // const html = fs.readFileSync('./temp.html', 'utf-8');
71
- // const markdown = fs.readFileSync('./temp.md', 'utf-8');
72
-
73
- // Measure time taken to process
74
- const startTime = process.hrtime();
75
- const result = processContent(html, markdown);
76
- const elapsed = process.hrtime(startTime);
77
- const timeInMs = elapsed[0] * 1000 + elapsed[1] / 1000000;
78
-
79
- console.log(
80
- `Processed ${result.links.length} links, ${result.images.length} images, and ${result.videos.length} videos in ${timeInMs.toFixed(2)}ms`
81
- );
82
-
83
- // Basic validations for large file processing
84
- expect(result.links.length).toBeGreaterThan(0);
85
- expect(result.markdown).toContain('link#');
86
-
87
- // Check if all links were replaced (sample check)
88
- expect(result.markdown).not.toContain('https://example.com/link');
89
- });
90
-
91
- // Memory usage test
92
- test('should have reasonable memory usage', () => {
93
- const html = fs.readFileSync('src/tools/search/test.html', 'utf-8');
94
- const markdown = fs.readFileSync('src/tools/search/test.md', 'utf-8');
95
-
96
- const beforeMem = process.memoryUsage();
97
- processContent(html, markdown);
98
- const afterMem = process.memoryUsage();
99
-
100
- const heapUsed = (afterMem.heapUsed - beforeMem.heapUsed) / 1024 / 1024; // MB
101
-
102
- console.log(`Memory used: ${heapUsed.toFixed(2)} MB`);
103
-
104
- // This is a loose check - actual thresholds depend on your environment
105
- expect(heapUsed).toBeLessThan(100); // Should use less than 100MB additional heap
106
- });
107
-
108
- // Real-world file test (if available)
109
- test('should process real-world Wikipedia content', () => {
110
- // Try to find real-world test files if they exist
111
- const wikiHtml = 'src/tools/search/test.html';
112
- const wikiMd = 'src/tools/search/test.md';
113
-
114
- if (fs.existsSync(wikiHtml) && fs.existsSync(wikiMd)) {
115
- const html = fs.readFileSync(wikiHtml, 'utf-8');
116
- const markdown = fs.readFileSync(wikiMd, 'utf-8');
117
-
118
- const result = processContent(html, markdown);
119
-
120
- console.log(
121
- `Processed ${result.links.length} Wikipedia links, ${result.images.length} images, and ${result.videos.length} videos`
122
- );
123
-
124
- expect(result.links.length).toBeGreaterThan(10); // Wikipedia articles typically have many links
125
- expect(result.markdown).not.toMatch(/\]\(https?:\/\/[^\s")]+\)/); // No regular URLs should remain
126
- } else {
127
- console.log('Wikipedia test files not found, skipping this test');
128
- }
129
- });
130
- });
131
-
132
- // Helper function to generate large HTML test data
133
- function generateLargeHtml(linkCount: number): string {
134
- let html = '<html><body>';
135
-
136
- for (let i = 1; i <= linkCount; i++) {
137
- html += `<p>Paragraph ${i} with <a href="https://example.com/link${i}" title="Link ${i}">link ${i}</a>`;
138
-
139
- if (i % 10 === 0) {
140
- html += ` and <img src="https://example.com/image${i / 10}.jpg" alt="Image ${i / 10}">`;
141
- }
142
-
143
- if (i % 50 === 0) {
144
- html += ` and <video src="https://example.com/video${i / 50}.mp4" title="Video ${i / 50}"></video>`;
145
- }
146
-
147
- html += '</p>';
148
- }
149
-
150
- html += '</body></html>';
151
- return html;
152
- }
153
-
154
- /** Helper function to generate large Markdown test data */
155
- function generateLargeMarkdown(linkCount: number): string {
156
- let markdown = '# Test Document\n\n';
157
-
158
- for (let i = 1; i <= linkCount; i++) {
159
- markdown += `Paragraph ${i} with [link ${i}](https://example.com/link${i} "Link ${i}")`;
160
-
161
- if (i % 10 === 0) {
162
- markdown += ` and ![Image ${i / 10}](https://example.com/image${i / 10}.jpg)`;
163
- }
164
-
165
- if (i % 50 === 0) {
166
- markdown += ` and [Video ${i / 50}](https://example.com/video${i / 50}.mp4 "Video ${i / 50}")`;
167
- }
168
-
169
- markdown += '\n\n';
170
- }
171
-
172
- return markdown;
173
- }
1
+ /* eslint-disable @typescript-eslint/no-unused-vars */
2
+ /* eslint-disable no-console */
3
+ // content.test.ts
4
+ import * as fs from 'fs';
5
+ import { processContent } from './content';
6
+
7
+ describe('Link Processor', () => {
8
+ afterAll(() => {
9
+ if (fs.existsSync('./temp.html')) {
10
+ fs.unlinkSync('./temp.html');
11
+ }
12
+ if (fs.existsSync('./temp.md')) {
13
+ fs.unlinkSync('./temp.md');
14
+ }
15
+ });
16
+ // Basic functionality tests
17
+ test('should replace basic links with references', () => {
18
+ const html = `
19
+ <p>Test with <a href="https://example.com/link" title="Example">a link</a></p>
20
+ <p>And an <img src="https://example.com/img.jpg" alt="image"></p>
21
+ <p>Plus a <video src="https://example.com/video.mp4"></video></p>
22
+ `;
23
+
24
+ const markdown = `
25
+ Test with [a link](https://example.com/link "Example")
26
+ And an ![image](https://example.com/img.jpg)
27
+ Plus a [video](https://example.com/video.mp4)
28
+ `;
29
+
30
+ const result = processContent(html, markdown);
31
+
32
+ expect(result.links.length).toBe(1);
33
+ expect(result.images.length).toBe(1);
34
+ expect(result.videos.length).toBe(1);
35
+ expect(result.markdown).toContain('link#1');
36
+ expect(result.markdown).toContain('image#1');
37
+ expect(result.markdown).toContain('video#1');
38
+ });
39
+
40
+ // Edge case tests
41
+ test('should handle links with parentheses and special characters', () => {
42
+ const html = `
43
+ <a href="https://example.com/page(1).html" title="Parens">Link with parens</a>
44
+ <a href="https://example.com/path?query=test&param=value">Link with query</a>
45
+ `;
46
+
47
+ const markdown = `
48
+ [Link with parens](https://example.com/page(1).html "Parens")
49
+ [Link with query](https://example.com/path?query=test&param=value)
50
+ `;
51
+
52
+ const result = processContent(html, markdown);
53
+
54
+ expect(result.links.length).toBe(2);
55
+ expect(result.markdown).toContain('link#1');
56
+ expect(result.markdown).toContain('link#2');
57
+ });
58
+
59
+ // Performance test with large files
60
+ test('should process large files efficiently', () => {
61
+ const html = fs.readFileSync('src/tools/search/test.html', 'utf-8');
62
+ const markdown = fs.readFileSync('src/tools/search/test.md', 'utf-8');
63
+
64
+ // const largeHtml = generateLargeHtml(1000); // 1000 links
65
+ // fs.writeFileSync('./temp.html', largeHtml);
66
+
67
+ // const largeMd = generateLargeMarkdown(1000); // 1000 links
68
+ // fs.writeFileSync('./temp.md', largeMd);
69
+
70
+ // const html = fs.readFileSync('./temp.html', 'utf-8');
71
+ // const markdown = fs.readFileSync('./temp.md', 'utf-8');
72
+
73
+ // Measure time taken to process
74
+ const startTime = process.hrtime();
75
+ const result = processContent(html, markdown);
76
+ const elapsed = process.hrtime(startTime);
77
+ const timeInMs = elapsed[0] * 1000 + elapsed[1] / 1000000;
78
+
79
+ console.log(
80
+ `Processed ${result.links.length} links, ${result.images.length} images, and ${result.videos.length} videos in ${timeInMs.toFixed(2)}ms`
81
+ );
82
+
83
+ // Basic validations for large file processing
84
+ expect(result.links.length).toBeGreaterThan(0);
85
+ expect(result.markdown).toContain('link#');
86
+
87
+ // Check if all links were replaced (sample check)
88
+ expect(result.markdown).not.toContain('https://example.com/link');
89
+ });
90
+
91
+ // Memory usage test
92
+ test('should have reasonable memory usage', () => {
93
+ const html = fs.readFileSync('src/tools/search/test.html', 'utf-8');
94
+ const markdown = fs.readFileSync('src/tools/search/test.md', 'utf-8');
95
+
96
+ const beforeMem = process.memoryUsage();
97
+ processContent(html, markdown);
98
+ const afterMem = process.memoryUsage();
99
+
100
+ const heapUsed = (afterMem.heapUsed - beforeMem.heapUsed) / 1024 / 1024; // MB
101
+
102
+ console.log(`Memory used: ${heapUsed.toFixed(2)} MB`);
103
+
104
+ // This is a loose check - actual thresholds depend on your environment
105
+ expect(heapUsed).toBeLessThan(100); // Should use less than 100MB additional heap
106
+ });
107
+
108
+ // Real-world file test (if available)
109
+ test('should process real-world Wikipedia content', () => {
110
+ // Try to find real-world test files if they exist
111
+ const wikiHtml = 'src/tools/search/test.html';
112
+ const wikiMd = 'src/tools/search/test.md';
113
+
114
+ if (fs.existsSync(wikiHtml) && fs.existsSync(wikiMd)) {
115
+ const html = fs.readFileSync(wikiHtml, 'utf-8');
116
+ const markdown = fs.readFileSync(wikiMd, 'utf-8');
117
+
118
+ const result = processContent(html, markdown);
119
+
120
+ console.log(
121
+ `Processed ${result.links.length} Wikipedia links, ${result.images.length} images, and ${result.videos.length} videos`
122
+ );
123
+
124
+ expect(result.links.length).toBeGreaterThan(10); // Wikipedia articles typically have many links
125
+ expect(result.markdown).not.toMatch(/\]\(https?:\/\/[^\s")]+\)/); // No regular URLs should remain
126
+ } else {
127
+ console.log('Wikipedia test files not found, skipping this test');
128
+ }
129
+ });
130
+ });
131
+
132
+ // Helper function to generate large HTML test data
133
+ function generateLargeHtml(linkCount: number): string {
134
+ let html = '<html><body>';
135
+
136
+ for (let i = 1; i <= linkCount; i++) {
137
+ html += `<p>Paragraph ${i} with <a href="https://example.com/link${i}" title="Link ${i}">link ${i}</a>`;
138
+
139
+ if (i % 10 === 0) {
140
+ html += ` and <img src="https://example.com/image${i / 10}.jpg" alt="Image ${i / 10}">`;
141
+ }
142
+
143
+ if (i % 50 === 0) {
144
+ html += ` and <video src="https://example.com/video${i / 50}.mp4" title="Video ${i / 50}"></video>`;
145
+ }
146
+
147
+ html += '</p>';
148
+ }
149
+
150
+ html += '</body></html>';
151
+ return html;
152
+ }
153
+
154
+ /** Helper function to generate large Markdown test data */
155
+ function generateLargeMarkdown(linkCount: number): string {
156
+ let markdown = '# Test Document\n\n';
157
+
158
+ for (let i = 1; i <= linkCount; i++) {
159
+ markdown += `Paragraph ${i} with [link ${i}](https://example.com/link${i} "Link ${i}")`;
160
+
161
+ if (i % 10 === 0) {
162
+ markdown += ` and ![Image ${i / 10}](https://example.com/image${i / 10}.jpg)`;
163
+ }
164
+
165
+ if (i % 50 === 0) {
166
+ markdown += ` and [Video ${i / 50}](https://example.com/video${i / 50}.mp4 "Video ${i / 50}")`;
167
+ }
168
+
169
+ markdown += '\n\n';
170
+ }
171
+
172
+ return markdown;
173
+ }