bluera-knowledge 0.9.32 → 0.9.36

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 (198) hide show
  1. package/.claude/hooks/post-edit-check.sh +5 -3
  2. package/.claude/skills/atomic-commits/SKILL.md +3 -1
  3. package/.husky/pre-commit +3 -2
  4. package/.prettierrc +9 -0
  5. package/.versionrc.json +1 -1
  6. package/CHANGELOG.md +70 -0
  7. package/CLAUDE.md +6 -0
  8. package/README.md +25 -13
  9. package/bun.lock +277 -33
  10. package/dist/{chunk-L2YVNC63.js → chunk-6FHWC36B.js} +9 -1
  11. package/dist/chunk-6FHWC36B.js.map +1 -0
  12. package/dist/{chunk-RST4XGRL.js → chunk-DC7CGSGT.js} +288 -241
  13. package/dist/chunk-DC7CGSGT.js.map +1 -0
  14. package/dist/{chunk-6PBP5DVD.js → chunk-WFNPNAAP.js} +3212 -3054
  15. package/dist/chunk-WFNPNAAP.js.map +1 -0
  16. package/dist/{chunk-WT2DAEO7.js → chunk-Z2KKVH45.js} +548 -482
  17. package/dist/chunk-Z2KKVH45.js.map +1 -0
  18. package/dist/index.js +871 -758
  19. package/dist/index.js.map +1 -1
  20. package/dist/mcp/server.js +3 -3
  21. package/dist/watch.service-BJV3TI3F.js +7 -0
  22. package/dist/workers/background-worker-cli.js +97 -71
  23. package/dist/workers/background-worker-cli.js.map +1 -1
  24. package/eslint.config.js +43 -1
  25. package/package.json +18 -11
  26. package/plugin.json +8 -0
  27. package/python/requirements.txt +1 -1
  28. package/src/analysis/ast-parser.test.ts +12 -11
  29. package/src/analysis/ast-parser.ts +28 -22
  30. package/src/analysis/code-graph.test.ts +52 -62
  31. package/src/analysis/code-graph.ts +9 -13
  32. package/src/analysis/dependency-usage-analyzer.test.ts +91 -271
  33. package/src/analysis/dependency-usage-analyzer.ts +52 -24
  34. package/src/analysis/go-ast-parser.test.ts +22 -22
  35. package/src/analysis/go-ast-parser.ts +18 -25
  36. package/src/analysis/parser-factory.test.ts +9 -9
  37. package/src/analysis/parser-factory.ts +3 -3
  38. package/src/analysis/python-ast-parser.test.ts +27 -27
  39. package/src/analysis/python-ast-parser.ts +2 -2
  40. package/src/analysis/repo-url-resolver.test.ts +82 -82
  41. package/src/analysis/rust-ast-parser.test.ts +19 -19
  42. package/src/analysis/rust-ast-parser.ts +17 -27
  43. package/src/analysis/tree-sitter-parser.test.ts +3 -3
  44. package/src/analysis/tree-sitter-parser.ts +10 -16
  45. package/src/cli/commands/crawl.test.ts +40 -24
  46. package/src/cli/commands/crawl.ts +186 -166
  47. package/src/cli/commands/index-cmd.test.ts +90 -90
  48. package/src/cli/commands/index-cmd.ts +52 -36
  49. package/src/cli/commands/mcp.test.ts +6 -6
  50. package/src/cli/commands/mcp.ts +2 -2
  51. package/src/cli/commands/plugin-api.test.ts +16 -18
  52. package/src/cli/commands/plugin-api.ts +9 -6
  53. package/src/cli/commands/search.test.ts +16 -7
  54. package/src/cli/commands/search.ts +124 -87
  55. package/src/cli/commands/serve.test.ts +67 -25
  56. package/src/cli/commands/serve.ts +18 -3
  57. package/src/cli/commands/setup.test.ts +176 -101
  58. package/src/cli/commands/setup.ts +140 -117
  59. package/src/cli/commands/store.test.ts +82 -53
  60. package/src/cli/commands/store.ts +56 -37
  61. package/src/cli/program.ts +2 -2
  62. package/src/crawl/article-converter.test.ts +4 -1
  63. package/src/crawl/article-converter.ts +46 -31
  64. package/src/crawl/bridge.test.ts +240 -132
  65. package/src/crawl/bridge.ts +87 -30
  66. package/src/crawl/claude-client.test.ts +124 -56
  67. package/src/crawl/claude-client.ts +7 -15
  68. package/src/crawl/intelligent-crawler.test.ts +65 -22
  69. package/src/crawl/intelligent-crawler.ts +86 -53
  70. package/src/crawl/markdown-utils.ts +1 -4
  71. package/src/db/embeddings.ts +4 -6
  72. package/src/db/lance.test.ts +4 -4
  73. package/src/db/lance.ts +16 -12
  74. package/src/index.ts +26 -17
  75. package/src/logging/index.ts +1 -5
  76. package/src/logging/logger.ts +3 -5
  77. package/src/logging/payload.test.ts +1 -1
  78. package/src/logging/payload.ts +3 -5
  79. package/src/mcp/commands/index.ts +2 -2
  80. package/src/mcp/commands/job.commands.ts +12 -18
  81. package/src/mcp/commands/meta.commands.ts +13 -13
  82. package/src/mcp/commands/registry.ts +5 -8
  83. package/src/mcp/commands/store.commands.ts +19 -19
  84. package/src/mcp/handlers/execute.handler.test.ts +10 -10
  85. package/src/mcp/handlers/execute.handler.ts +4 -5
  86. package/src/mcp/handlers/index.ts +10 -14
  87. package/src/mcp/handlers/job.handler.test.ts +10 -10
  88. package/src/mcp/handlers/job.handler.ts +22 -25
  89. package/src/mcp/handlers/search.handler.test.ts +36 -65
  90. package/src/mcp/handlers/search.handler.ts +135 -104
  91. package/src/mcp/handlers/store.handler.test.ts +41 -52
  92. package/src/mcp/handlers/store.handler.ts +108 -88
  93. package/src/mcp/schemas/index.test.ts +73 -68
  94. package/src/mcp/schemas/index.ts +18 -12
  95. package/src/mcp/server.test.ts +1 -1
  96. package/src/mcp/server.ts +59 -46
  97. package/src/plugin/commands.test.ts +230 -95
  98. package/src/plugin/commands.ts +24 -25
  99. package/src/plugin/dependency-analyzer.test.ts +52 -52
  100. package/src/plugin/dependency-analyzer.ts +85 -22
  101. package/src/plugin/git-clone.test.ts +24 -13
  102. package/src/plugin/git-clone.ts +3 -7
  103. package/src/server/app.test.ts +109 -109
  104. package/src/server/app.ts +32 -23
  105. package/src/server/index.test.ts +64 -66
  106. package/src/services/chunking.service.test.ts +32 -32
  107. package/src/services/chunking.service.ts +16 -9
  108. package/src/services/code-graph.service.test.ts +30 -36
  109. package/src/services/code-graph.service.ts +24 -10
  110. package/src/services/code-unit.service.test.ts +55 -11
  111. package/src/services/code-unit.service.ts +85 -11
  112. package/src/services/config.service.test.ts +37 -18
  113. package/src/services/config.service.ts +30 -7
  114. package/src/services/index.service.test.ts +49 -18
  115. package/src/services/index.service.ts +98 -48
  116. package/src/services/index.ts +6 -9
  117. package/src/services/job.service.test.ts +22 -22
  118. package/src/services/job.service.ts +18 -18
  119. package/src/services/project-root.service.test.ts +1 -3
  120. package/src/services/search.service.test.ts +248 -120
  121. package/src/services/search.service.ts +286 -156
  122. package/src/services/services.test.ts +1 -1
  123. package/src/services/snippet.service.test.ts +14 -6
  124. package/src/services/snippet.service.ts +7 -5
  125. package/src/services/store.service.test.ts +68 -29
  126. package/src/services/store.service.ts +41 -12
  127. package/src/services/watch.service.test.ts +34 -14
  128. package/src/services/watch.service.ts +11 -1
  129. package/src/types/brands.test.ts +3 -1
  130. package/src/types/index.ts +2 -13
  131. package/src/types/search.ts +10 -8
  132. package/src/utils/type-guards.test.ts +20 -15
  133. package/src/utils/type-guards.ts +1 -1
  134. package/src/workers/background-worker-cli.ts +28 -30
  135. package/src/workers/background-worker.test.ts +54 -40
  136. package/src/workers/background-worker.ts +76 -60
  137. package/src/workers/pid-file.test.ts +167 -0
  138. package/src/workers/pid-file.ts +82 -0
  139. package/src/workers/spawn-worker.test.ts +22 -10
  140. package/src/workers/spawn-worker.ts +6 -6
  141. package/tests/analysis/ast-parser.test.ts +3 -3
  142. package/tests/analysis/code-graph.test.ts +5 -5
  143. package/tests/fixtures/code-snippets/api/error-handling.ts +4 -15
  144. package/tests/fixtures/code-snippets/api/rest-controller.ts +3 -9
  145. package/tests/fixtures/code-snippets/auth/jwt-auth.ts +5 -21
  146. package/tests/fixtures/code-snippets/auth/oauth-flow.ts +4 -4
  147. package/tests/fixtures/code-snippets/database/repository-pattern.ts +11 -3
  148. package/tests/fixtures/corpus/oss-repos/hono/src/adapter/aws-lambda/handler.ts +2 -2
  149. package/tests/fixtures/corpus/oss-repos/hono/src/adapter/cloudflare-pages/handler.ts +1 -1
  150. package/tests/fixtures/corpus/oss-repos/hono/src/adapter/cloudflare-workers/serve-static.ts +2 -2
  151. package/tests/fixtures/corpus/oss-repos/hono/src/client/client.ts +2 -2
  152. package/tests/fixtures/corpus/oss-repos/hono/src/client/types.ts +22 -20
  153. package/tests/fixtures/corpus/oss-repos/hono/src/context.ts +13 -10
  154. package/tests/fixtures/corpus/oss-repos/hono/src/helper/accepts/accepts.ts +10 -7
  155. package/tests/fixtures/corpus/oss-repos/hono/src/helper/adapter/index.ts +2 -2
  156. package/tests/fixtures/corpus/oss-repos/hono/src/helper/css/index.ts +1 -1
  157. package/tests/fixtures/corpus/oss-repos/hono/src/helper/factory/index.ts +16 -16
  158. package/tests/fixtures/corpus/oss-repos/hono/src/helper/ssg/ssg.ts +2 -2
  159. package/tests/fixtures/corpus/oss-repos/hono/src/hono-base.ts +3 -3
  160. package/tests/fixtures/corpus/oss-repos/hono/src/hono.ts +1 -1
  161. package/tests/fixtures/corpus/oss-repos/hono/src/jsx/dom/css.ts +2 -2
  162. package/tests/fixtures/corpus/oss-repos/hono/src/jsx/dom/intrinsic-element/components.ts +1 -1
  163. package/tests/fixtures/corpus/oss-repos/hono/src/jsx/dom/render.ts +7 -7
  164. package/tests/fixtures/corpus/oss-repos/hono/src/jsx/hooks/index.ts +3 -3
  165. package/tests/fixtures/corpus/oss-repos/hono/src/jsx/intrinsic-element/components.ts +1 -1
  166. package/tests/fixtures/corpus/oss-repos/hono/src/jsx/utils.ts +6 -6
  167. package/tests/fixtures/corpus/oss-repos/hono/src/middleware/jsx-renderer/index.ts +3 -3
  168. package/tests/fixtures/corpus/oss-repos/hono/src/middleware/serve-static/index.ts +1 -1
  169. package/tests/fixtures/corpus/oss-repos/hono/src/preset/quick.ts +1 -1
  170. package/tests/fixtures/corpus/oss-repos/hono/src/preset/tiny.ts +1 -1
  171. package/tests/fixtures/corpus/oss-repos/hono/src/router/pattern-router/router.ts +2 -2
  172. package/tests/fixtures/corpus/oss-repos/hono/src/router/reg-exp-router/node.ts +4 -4
  173. package/tests/fixtures/corpus/oss-repos/hono/src/router/reg-exp-router/router.ts +1 -1
  174. package/tests/fixtures/corpus/oss-repos/hono/src/router/trie-router/node.ts +1 -1
  175. package/tests/fixtures/corpus/oss-repos/hono/src/types.ts +166 -169
  176. package/tests/fixtures/corpus/oss-repos/hono/src/utils/body.ts +8 -8
  177. package/tests/fixtures/corpus/oss-repos/hono/src/utils/color.ts +3 -3
  178. package/tests/fixtures/corpus/oss-repos/hono/src/utils/cookie.ts +2 -2
  179. package/tests/fixtures/corpus/oss-repos/hono/src/utils/encode.ts +2 -2
  180. package/tests/fixtures/corpus/oss-repos/hono/src/utils/types.ts +30 -33
  181. package/tests/fixtures/corpus/oss-repos/hono/src/validator/validator.ts +2 -2
  182. package/tests/fixtures/test-server.ts +3 -2
  183. package/tests/helpers/performance-metrics.ts +8 -25
  184. package/tests/helpers/search-relevance.ts +14 -69
  185. package/tests/integration/cli-consistency.test.ts +6 -5
  186. package/tests/integration/python-bridge.test.ts +13 -3
  187. package/tests/mcp/server.test.ts +1 -1
  188. package/tests/services/code-unit.service.test.ts +48 -0
  189. package/tests/services/job.service.test.ts +124 -0
  190. package/tests/services/search.progressive-context.test.ts +2 -2
  191. package/.claude-plugin/plugin.json +0 -13
  192. package/dist/chunk-6PBP5DVD.js.map +0 -1
  193. package/dist/chunk-L2YVNC63.js.map +0 -1
  194. package/dist/chunk-RST4XGRL.js.map +0 -1
  195. package/dist/chunk-WT2DAEO7.js.map +0 -1
  196. package/dist/watch.service-YAIKKDCF.js +0 -7
  197. package/skills/atomic-commits/SKILL.md +0 -77
  198. /package/dist/{watch.service-YAIKKDCF.js.map → watch.service-BJV3TI3F.js.map} +0 -0
@@ -6,6 +6,7 @@
6
6
  import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
7
7
  import { PythonBridge } from './bridge.js';
8
8
  import type { ChildProcess } from 'node:child_process';
9
+ import type { Interface as ReadlineInterface, ReadLineOptions } from 'node:readline';
9
10
  import { EventEmitter } from 'node:events';
10
11
 
11
12
  // Mock child_process
@@ -27,17 +28,27 @@ describe('PythonBridge', () => {
27
28
  let mockReadline: MockReadline;
28
29
  let mockStderrReadline: MockReadline;
29
30
 
31
+ // Mock classes that satisfy the ChildProcess and Interface contracts
30
32
  class MockChildProcess extends EventEmitter {
31
- stdin = {
32
- write: vi.fn(),
33
- };
33
+ stdin = { write: vi.fn() };
34
34
  stdout = new EventEmitter();
35
35
  stderr = new EventEmitter();
36
- kill = vi.fn();
36
+ kill = vi.fn(() => {
37
+ // Emit exit event asynchronously to simulate real process behavior
38
+ setImmediate(() => this.emit('exit', 0, null));
39
+ });
40
+ // Type-safe cast helper
41
+ asChildProcess(): ChildProcess {
42
+ return this as unknown as ChildProcess;
43
+ }
37
44
  }
38
45
 
39
46
  class MockReadline extends EventEmitter {
40
47
  close = vi.fn();
48
+ // Type-safe cast helper
49
+ asInterface(): ReadlineInterface {
50
+ return this as unknown as ReadlineInterface;
51
+ }
41
52
  }
42
53
 
43
54
  beforeEach(() => {
@@ -47,14 +58,13 @@ describe('PythonBridge', () => {
47
58
  mockReadline = new MockReadline();
48
59
  mockStderrReadline = new MockReadline();
49
60
 
50
- vi.mocked(spawn).mockReturnValue(mockProcess as unknown as ChildProcess);
51
- // Use mockImplementation to create new instances each time
52
- vi.mocked(createInterface).mockImplementation((config: any) => {
61
+ vi.mocked(spawn).mockReturnValue(mockProcess.asChildProcess());
62
+ vi.mocked(createInterface).mockImplementation((config: ReadLineOptions) => {
53
63
  // First call is for stderr, second is for stdout
54
64
  if (config.input === mockProcess.stderr) {
55
- return mockStderrReadline as any;
65
+ return mockStderrReadline.asInterface();
56
66
  }
57
- return mockReadline as any;
67
+ return mockReadline.asInterface();
58
68
  });
59
69
  });
60
70
 
@@ -71,7 +81,7 @@ describe('PythonBridge', () => {
71
81
  ['python/crawl_worker.py'],
72
82
  expect.objectContaining({
73
83
  stdio: ['pipe', 'pipe', 'pipe'],
74
- }),
84
+ })
75
85
  );
76
86
  });
77
87
 
@@ -89,7 +99,7 @@ describe('PythonBridge', () => {
89
99
  expect(createInterface).toHaveBeenCalledWith(
90
100
  expect.objectContaining({
91
101
  input: mockProcess.stdout,
92
- }),
102
+ })
93
103
  );
94
104
  });
95
105
 
@@ -99,7 +109,7 @@ describe('PythonBridge', () => {
99
109
  expect(createInterface).toHaveBeenCalledWith(
100
110
  expect.objectContaining({
101
111
  input: mockProcess.stderr,
102
- }),
112
+ })
103
113
  );
104
114
  });
105
115
 
@@ -107,13 +117,16 @@ describe('PythonBridge', () => {
107
117
  const promise = bridge.crawl('https://example.com');
108
118
 
109
119
  // Wait for process to start and write
110
- await new Promise(resolve => setImmediate(resolve));
120
+ await new Promise((resolve) => setImmediate(resolve));
111
121
 
112
122
  const request = JSON.parse(vi.mocked(mockProcess.stdin.write).mock.calls[0]?.[0] as string);
113
- mockReadline.emit('line', JSON.stringify({
114
- id: request.id,
115
- result: { pages: [] },
116
- }));
123
+ mockReadline.emit(
124
+ 'line',
125
+ JSON.stringify({
126
+ id: request.id,
127
+ result: { pages: [] },
128
+ })
129
+ );
117
130
 
118
131
  await promise;
119
132
  expect(spawn).toHaveBeenCalled();
@@ -127,18 +140,21 @@ describe('PythonBridge', () => {
127
140
 
128
141
  // Emit response immediately
129
142
  const request = JSON.parse(vi.mocked(mockProcess.stdin.write).mock.calls[0]?.[0] as string);
130
- mockReadline.emit('line', JSON.stringify({
131
- id: request.id,
132
- result: { pages: [] },
133
- }));
143
+ mockReadline.emit(
144
+ 'line',
145
+ JSON.stringify({
146
+ id: request.id,
147
+ result: { pages: [] },
148
+ })
149
+ );
134
150
 
135
151
  await promise;
136
152
 
137
153
  expect(mockProcess.stdin.write).toHaveBeenCalledWith(
138
- expect.stringContaining('"jsonrpc":"2.0"'),
154
+ expect.stringContaining('"jsonrpc":"2.0"')
139
155
  );
140
156
  expect(mockProcess.stdin.write).toHaveBeenCalledWith(
141
- expect.stringContaining('"method":"crawl"'),
157
+ expect.stringContaining('"method":"crawl"')
142
158
  );
143
159
  });
144
160
 
@@ -147,14 +163,19 @@ describe('PythonBridge', () => {
147
163
  const promise = bridge.crawl('https://example.com/test');
148
164
 
149
165
  const request = JSON.parse(vi.mocked(mockProcess.stdin.write).mock.calls[0]?.[0] as string);
150
- mockReadline.emit('line', JSON.stringify({
151
- id: request.id,
152
- result: { pages: [] },
153
- }));
166
+ mockReadline.emit(
167
+ 'line',
168
+ JSON.stringify({
169
+ id: request.id,
170
+ result: { pages: [] },
171
+ })
172
+ );
154
173
 
155
174
  await promise;
156
175
 
157
- expect(vi.mocked(mockProcess.stdin.write).mock.calls[0]?.[0]).toContain('"url":"https://example.com/test"');
176
+ expect(vi.mocked(mockProcess.stdin.write).mock.calls[0]?.[0]).toContain(
177
+ '"url":"https://example.com/test"'
178
+ );
158
179
  });
159
180
 
160
181
  it('should generate unique request IDs', async () => {
@@ -184,14 +205,28 @@ describe('PythonBridge', () => {
184
205
  const req2 = JSON.parse(vi.mocked(mockProcess.stdin.write).mock.calls[1]?.[0] as string);
185
206
 
186
207
  // Send responses in reverse order
187
- mockReadline.emit('line', JSON.stringify({
188
- id: req2.id,
189
- result: { pages: [{ url: 'url2', title: 't2', content: 'c2', links: [], crawledAt: '2024-01-02' }] },
190
- }));
191
- mockReadline.emit('line', JSON.stringify({
192
- id: req1.id,
193
- result: { pages: [{ url: 'url1', title: 't1', content: 'c1', links: [], crawledAt: '2024-01-01' }] },
194
- }));
208
+ mockReadline.emit(
209
+ 'line',
210
+ JSON.stringify({
211
+ id: req2.id,
212
+ result: {
213
+ pages: [
214
+ { url: 'url2', title: 't2', content: 'c2', links: [], crawledAt: '2024-01-02' },
215
+ ],
216
+ },
217
+ })
218
+ );
219
+ mockReadline.emit(
220
+ 'line',
221
+ JSON.stringify({
222
+ id: req1.id,
223
+ result: {
224
+ pages: [
225
+ { url: 'url1', title: 't1', content: 'c1', links: [], crawledAt: '2024-01-01' },
226
+ ],
227
+ },
228
+ })
229
+ );
195
230
 
196
231
  const [result1, result2] = await Promise.all([promise1, promise2]);
197
232
 
@@ -204,18 +239,23 @@ describe('PythonBridge', () => {
204
239
  const promise = bridge.crawl('https://example.com');
205
240
 
206
241
  const request = JSON.parse(vi.mocked(mockProcess.stdin.write).mock.calls[0]?.[0] as string);
207
- mockReadline.emit('line', JSON.stringify({
208
- id: request.id,
209
- result: {
210
- pages: [{
211
- url: 'https://example.com',
212
- title: 'Test',
213
- content: 'Content',
214
- links: [],
215
- crawledAt: '2024-01-01',
216
- }],
217
- },
218
- }));
242
+ mockReadline.emit(
243
+ 'line',
244
+ JSON.stringify({
245
+ id: request.id,
246
+ result: {
247
+ pages: [
248
+ {
249
+ url: 'https://example.com',
250
+ title: 'Test',
251
+ content: 'Content',
252
+ links: [],
253
+ crawledAt: '2024-01-01',
254
+ },
255
+ ],
256
+ },
257
+ })
258
+ );
219
259
 
220
260
  const result = await promise;
221
261
  expect(result.pages).toHaveLength(1);
@@ -227,10 +267,13 @@ describe('PythonBridge', () => {
227
267
  const promise = bridge.crawl('https://example.com');
228
268
 
229
269
  const request = JSON.parse(vi.mocked(mockProcess.stdin.write).mock.calls[0]?.[0] as string);
230
- mockReadline.emit('line', JSON.stringify({
231
- id: request.id,
232
- error: { message: 'Crawl failed' },
233
- }));
270
+ mockReadline.emit(
271
+ 'line',
272
+ JSON.stringify({
273
+ id: request.id,
274
+ error: { message: 'Crawl failed' },
275
+ })
276
+ );
234
277
 
235
278
  await expect(promise).rejects.toThrow('Crawl failed');
236
279
  });
@@ -251,10 +294,13 @@ describe('PythonBridge', () => {
251
294
 
252
295
  // Should not timeout immediately - resolve quickly
253
296
  const request = JSON.parse(vi.mocked(mockProcess.stdin.write).mock.calls[0]?.[0] as string);
254
- mockReadline.emit('line', JSON.stringify({
255
- id: request.id,
256
- result: { pages: [] },
257
- }));
297
+ mockReadline.emit(
298
+ 'line',
299
+ JSON.stringify({
300
+ id: request.id,
301
+ result: { pages: [] },
302
+ })
303
+ );
258
304
 
259
305
  await expect(promise).resolves.toBeDefined();
260
306
  });
@@ -264,10 +310,13 @@ describe('PythonBridge', () => {
264
310
  const promise = bridge.crawl('https://example.com', 5000);
265
311
 
266
312
  const request = JSON.parse(vi.mocked(mockProcess.stdin.write).mock.calls[0]?.[0] as string);
267
- mockReadline.emit('line', JSON.stringify({
268
- id: request.id,
269
- result: { pages: [] },
270
- }));
313
+ mockReadline.emit(
314
+ 'line',
315
+ JSON.stringify({
316
+ id: request.id,
317
+ result: { pages: [] },
318
+ })
319
+ );
271
320
 
272
321
  await promise;
273
322
 
@@ -280,10 +329,13 @@ describe('PythonBridge', () => {
280
329
  const promise = bridge.crawl('https://example.com', 5000);
281
330
 
282
331
  const request = JSON.parse(vi.mocked(mockProcess.stdin.write).mock.calls[0]?.[0] as string);
283
- mockReadline.emit('line', JSON.stringify({
284
- id: request.id,
285
- error: { message: 'Error' },
286
- }));
332
+ mockReadline.emit(
333
+ 'line',
334
+ JSON.stringify({
335
+ id: request.id,
336
+ error: { message: 'Error' },
337
+ })
338
+ );
287
339
 
288
340
  await expect(promise).rejects.toThrow();
289
341
  });
@@ -297,10 +349,13 @@ describe('PythonBridge', () => {
297
349
  // Try to send a late response - should be ignored
298
350
  const request = JSON.parse(vi.mocked(mockProcess.stdin.write).mock.calls[0]?.[0] as string);
299
351
  expect(() => {
300
- mockReadline.emit('line', JSON.stringify({
301
- id: request.id,
302
- result: { pages: [] },
303
- }));
352
+ mockReadline.emit(
353
+ 'line',
354
+ JSON.stringify({
355
+ id: request.id,
356
+ result: { pages: [] },
357
+ })
358
+ );
304
359
  }).not.toThrow();
305
360
  });
306
361
  });
@@ -311,10 +366,13 @@ describe('PythonBridge', () => {
311
366
  const promise = bridge.crawl('https://example.com');
312
367
 
313
368
  const request = JSON.parse(vi.mocked(mockProcess.stdin.write).mock.calls[0]?.[0] as string);
314
- mockReadline.emit('line', JSON.stringify({
315
- id: request.id,
316
- result: { pages: [] },
317
- }));
369
+ mockReadline.emit(
370
+ 'line',
371
+ JSON.stringify({
372
+ id: request.id,
373
+ result: { pages: [] },
374
+ })
375
+ );
318
376
 
319
377
  await expect(promise).resolves.toBeDefined();
320
378
  });
@@ -333,10 +391,13 @@ describe('PythonBridge', () => {
333
391
  await bridge.start();
334
392
  const promise = bridge.crawl('https://example.com', 100);
335
393
 
336
- mockReadline.emit('line', JSON.stringify({
337
- id: 'unknown-id',
338
- result: { pages: [] },
339
- }));
394
+ mockReadline.emit(
395
+ 'line',
396
+ JSON.stringify({
397
+ id: 'unknown-id',
398
+ result: { pages: [] },
399
+ })
400
+ );
340
401
 
341
402
  // Should timeout since response wasn't matched
342
403
  await expect(promise).rejects.toThrow('timeout');
@@ -394,20 +455,25 @@ describe('PythonBridge', () => {
394
455
  const newMockReadline = new MockReadline();
395
456
  const newMockStderrReadline = new MockReadline();
396
457
 
397
- vi.mocked(spawn).mockReturnValue(newMockProcess as unknown as ChildProcess);
458
+ vi.mocked(spawn).mockReturnValue(newMockProcess.asChildProcess());
398
459
  vi.mocked(createInterface)
399
- .mockReturnValueOnce(newMockStderrReadline as any)
400
- .mockReturnValueOnce(newMockReadline as any);
460
+ .mockReturnValueOnce(newMockStderrReadline.asInterface())
461
+ .mockReturnValueOnce(newMockReadline.asInterface());
401
462
 
402
463
  const promise = bridge.crawl('https://example.com');
403
464
 
404
- await new Promise(resolve => setImmediate(resolve));
465
+ await new Promise((resolve) => setImmediate(resolve));
405
466
 
406
- const request = JSON.parse(vi.mocked(newMockProcess.stdin.write).mock.calls[0]?.[0] as string);
407
- newMockReadline.emit('line', JSON.stringify({
408
- id: request.id,
409
- result: { pages: [] },
410
- }));
467
+ const request = JSON.parse(
468
+ vi.mocked(newMockProcess.stdin.write).mock.calls[0]?.[0] as string
469
+ );
470
+ newMockReadline.emit(
471
+ 'line',
472
+ JSON.stringify({
473
+ id: request.id,
474
+ result: { pages: [] },
475
+ })
476
+ );
411
477
 
412
478
  await promise;
413
479
  expect(spawn).toHaveBeenCalledTimes(2);
@@ -415,11 +481,25 @@ describe('PythonBridge', () => {
415
481
 
416
482
  it('should handle process with null stdout', async () => {
417
483
  const nullStdoutProcess = new MockChildProcess();
418
- (nullStdoutProcess as any).stdout = null;
484
+ // Override stdout to null for this test case
485
+ Object.defineProperty(nullStdoutProcess, 'stdout', { value: null });
486
+
487
+ vi.mocked(spawn).mockReturnValue(nullStdoutProcess.asChildProcess());
488
+
489
+ await expect(bridge.start()).rejects.toThrow('Python bridge process stdout is null');
490
+ });
491
+
492
+ it('should kill process when stdout is null to prevent zombie process', async () => {
493
+ const nullStdoutProcess = new MockChildProcess();
494
+ // Override stdout to null for this test case
495
+ Object.defineProperty(nullStdoutProcess, 'stdout', { value: null });
419
496
 
420
- vi.mocked(spawn).mockReturnValue(nullStdoutProcess as unknown as ChildProcess);
497
+ vi.mocked(spawn).mockReturnValue(nullStdoutProcess.asChildProcess());
421
498
 
422
499
  await expect(bridge.start()).rejects.toThrow('Python bridge process stdout is null');
500
+
501
+ // Critical: process must be killed to prevent zombie
502
+ expect(nullStdoutProcess.kill).toHaveBeenCalled();
423
503
  });
424
504
  });
425
505
 
@@ -429,23 +509,29 @@ describe('PythonBridge', () => {
429
509
  let capturedId: string | undefined;
430
510
  const promise = bridge.crawl('https://example.com');
431
511
 
432
- await new Promise(resolve => setImmediate(resolve));
512
+ await new Promise((resolve) => setImmediate(resolve));
433
513
 
434
514
  const request = JSON.parse(vi.mocked(mockProcess.stdin.write).mock.calls[0]?.[0] as string);
435
515
  capturedId = request.id;
436
- mockReadline.emit('line', JSON.stringify({
437
- id: request.id,
438
- result: { pages: [] },
439
- }));
516
+ mockReadline.emit(
517
+ 'line',
518
+ JSON.stringify({
519
+ id: request.id,
520
+ result: { pages: [] },
521
+ })
522
+ );
440
523
 
441
524
  await promise;
442
525
 
443
526
  // Send same ID again - should be ignored (pending was cleared)
444
527
  expect(() => {
445
- mockReadline.emit('line', JSON.stringify({
446
- id: capturedId,
447
- result: { pages: [] },
448
- }));
528
+ mockReadline.emit(
529
+ 'line',
530
+ JSON.stringify({
531
+ id: capturedId,
532
+ result: { pages: [] },
533
+ })
534
+ );
449
535
  }).not.toThrow();
450
536
  });
451
537
 
@@ -454,23 +540,29 @@ describe('PythonBridge', () => {
454
540
  let capturedId: string | undefined;
455
541
  const promise = bridge.crawl('https://example.com');
456
542
 
457
- await new Promise(resolve => setImmediate(resolve));
543
+ await new Promise((resolve) => setImmediate(resolve));
458
544
 
459
545
  const request = JSON.parse(vi.mocked(mockProcess.stdin.write).mock.calls[0]?.[0] as string);
460
546
  capturedId = request.id;
461
- mockReadline.emit('line', JSON.stringify({
462
- id: request.id,
463
- error: { message: 'Error' },
464
- }));
547
+ mockReadline.emit(
548
+ 'line',
549
+ JSON.stringify({
550
+ id: request.id,
551
+ error: { message: 'Error' },
552
+ })
553
+ );
465
554
 
466
555
  await expect(promise).rejects.toThrow();
467
556
 
468
557
  // Pending should be cleared
469
558
  expect(() => {
470
- mockReadline.emit('line', JSON.stringify({
471
- id: capturedId,
472
- result: { pages: [] },
473
- }));
559
+ mockReadline.emit(
560
+ 'line',
561
+ JSON.stringify({
562
+ id: capturedId,
563
+ result: { pages: [] },
564
+ })
565
+ );
474
566
  }).not.toThrow();
475
567
  });
476
568
 
@@ -483,10 +575,13 @@ describe('PythonBridge', () => {
483
575
  // Pending should be cleared, late response should be ignored
484
576
  const request = JSON.parse(vi.mocked(mockProcess.stdin.write).mock.calls[0]?.[0] as string);
485
577
  expect(() => {
486
- mockReadline.emit('line', JSON.stringify({
487
- id: request.id,
488
- result: { pages: [] },
489
- }));
578
+ mockReadline.emit(
579
+ 'line',
580
+ JSON.stringify({
581
+ id: request.id,
582
+ result: { pages: [] },
583
+ })
584
+ );
490
585
  }).not.toThrow();
491
586
  });
492
587
 
@@ -495,10 +590,14 @@ describe('PythonBridge', () => {
495
590
  const promise1 = bridge.crawl('https://example.com/1', 1000);
496
591
  const promise2 = bridge.crawl('https://example.com/2', 1000);
497
592
 
593
+ // Attach rejection handlers BEFORE stop to avoid unhandled rejection
594
+ const rejection1 = expect(promise1).rejects.toThrow('stopped');
595
+ const rejection2 = expect(promise2).rejects.toThrow('stopped');
596
+
498
597
  await bridge.stop();
499
598
 
500
- await expect(promise1).rejects.toThrow('stopped');
501
- await expect(promise2).rejects.toThrow('stopped');
599
+ await rejection1;
600
+ await rejection2;
502
601
  });
503
602
  });
504
603
 
@@ -512,7 +611,7 @@ describe('PythonBridge', () => {
512
611
  bridge.crawl('https://example.com/3'),
513
612
  ];
514
613
 
515
- await new Promise(resolve => setImmediate(resolve));
614
+ await new Promise((resolve) => setImmediate(resolve));
516
615
 
517
616
  const req1 = JSON.parse(vi.mocked(mockProcess.stdin.write).mock.calls[0]?.[0] as string);
518
617
  const req2 = JSON.parse(vi.mocked(mockProcess.stdin.write).mock.calls[1]?.[0] as string);
@@ -532,7 +631,7 @@ describe('PythonBridge', () => {
532
631
  const promise1 = bridge.crawl('https://example.com/1', 50);
533
632
  const promise2 = bridge.crawl('https://example.com/2', 1000);
534
633
 
535
- await new Promise(resolve => setImmediate(resolve));
634
+ await new Promise((resolve) => setImmediate(resolve));
536
635
 
537
636
  // Resolve second immediately
538
637
  const req2 = JSON.parse(vi.mocked(mockProcess.stdin.write).mock.calls[1]?.[0] as string);
@@ -564,10 +663,10 @@ describe('PythonBridge', () => {
564
663
  const newMockReadline = new MockReadline();
565
664
  const newMockStderrReadline = new MockReadline();
566
665
 
567
- vi.mocked(spawn).mockReturnValue(newMockProcess as unknown as ChildProcess);
666
+ vi.mocked(spawn).mockReturnValue(newMockProcess.asChildProcess());
568
667
  vi.mocked(createInterface)
569
- .mockReturnValueOnce(newMockStderrReadline as any)
570
- .mockReturnValueOnce(newMockReadline as any);
668
+ .mockReturnValueOnce(newMockStderrReadline.asInterface())
669
+ .mockReturnValueOnce(newMockReadline.asInterface());
571
670
 
572
671
  await bridge.start();
573
672
 
@@ -583,9 +682,12 @@ describe('PythonBridge', () => {
583
682
  await bridge.start();
584
683
  const promise = bridge.crawl('https://example.com');
585
684
 
685
+ // Attach rejection handler BEFORE stop to avoid unhandled rejection
686
+ const rejection = expect(promise).rejects.toThrow('Python bridge stopped');
687
+
586
688
  await bridge.stop();
587
689
 
588
- await expect(promise).rejects.toThrow('Python bridge stopped');
690
+ await rejection;
589
691
  });
590
692
  });
591
693
 
@@ -595,7 +697,7 @@ describe('PythonBridge', () => {
595
697
  mockProcess.emit('exit', 0, null);
596
698
 
597
699
  // Should be able to start again
598
- vi.mocked(spawn).mockReturnValue(mockProcess as unknown as ChildProcess);
700
+ vi.mocked(spawn).mockReturnValue(mockProcess.asChildProcess());
599
701
  await bridge.start();
600
702
 
601
703
  expect(spawn).toHaveBeenCalledTimes(2);
@@ -609,10 +711,10 @@ describe('PythonBridge', () => {
609
711
  const newMockReadline = new MockReadline();
610
712
  const newMockStderrReadline = new MockReadline();
611
713
 
612
- vi.mocked(spawn).mockReturnValue(newMockProcess as unknown as ChildProcess);
714
+ vi.mocked(spawn).mockReturnValue(newMockProcess.asChildProcess());
613
715
  vi.mocked(createInterface)
614
- .mockReturnValueOnce(newMockStderrReadline as any)
615
- .mockReturnValueOnce(newMockReadline as any);
716
+ .mockReturnValueOnce(newMockStderrReadline.asInterface())
717
+ .mockReturnValueOnce(newMockReadline.asInterface());
616
718
 
617
719
  await bridge.start();
618
720
 
@@ -631,20 +733,25 @@ describe('PythonBridge', () => {
631
733
  const newMockReadline = new MockReadline();
632
734
  const newMockStderrReadline = new MockReadline();
633
735
 
634
- vi.mocked(spawn).mockReturnValue(newMockProcess as unknown as ChildProcess);
736
+ vi.mocked(spawn).mockReturnValue(newMockProcess.asChildProcess());
635
737
  vi.mocked(createInterface)
636
- .mockReturnValueOnce(newMockStderrReadline as any)
637
- .mockReturnValueOnce(newMockReadline as any);
738
+ .mockReturnValueOnce(newMockStderrReadline.asInterface())
739
+ .mockReturnValueOnce(newMockReadline.asInterface());
638
740
 
639
741
  const promise = bridge.crawl('https://example.com');
640
742
 
641
- await new Promise(resolve => setImmediate(resolve));
743
+ await new Promise((resolve) => setImmediate(resolve));
642
744
 
643
- const request = JSON.parse(vi.mocked(newMockProcess.stdin.write).mock.calls[0]?.[0] as string);
644
- newMockReadline.emit('line', JSON.stringify({
645
- id: request.id,
646
- result: { pages: [] },
647
- }));
745
+ const request = JSON.parse(
746
+ vi.mocked(newMockProcess.stdin.write).mock.calls[0]?.[0] as string
747
+ );
748
+ newMockReadline.emit(
749
+ 'line',
750
+ JSON.stringify({
751
+ id: request.id,
752
+ result: { pages: [] },
753
+ })
754
+ );
648
755
 
649
756
  await expect(promise).resolves.toBeDefined();
650
757
  });
@@ -653,7 +760,8 @@ describe('PythonBridge', () => {
653
760
  describe('Error Edge Cases', () => {
654
761
  it('should handle crawl when process stdin is null', async () => {
655
762
  await bridge.start();
656
- (mockProcess as any).stdin = null;
763
+ // Override stdin to null for this test case
764
+ Object.defineProperty(mockProcess, 'stdin', { value: null });
657
765
 
658
766
  await expect(bridge.crawl('https://example.com')).rejects.toThrow('process not available');
659
767
  });
@@ -662,7 +770,7 @@ describe('PythonBridge', () => {
662
770
  await bridge.start();
663
771
  const promise = bridge.crawl('https://example.com', 100);
664
772
 
665
- await new Promise(resolve => setImmediate(resolve));
773
+ await new Promise((resolve) => setImmediate(resolve));
666
774
 
667
775
  const request = JSON.parse(vi.mocked(mockProcess.stdin.write).mock.calls[0]?.[0] as string);
668
776
  mockReadline.emit('line', JSON.stringify({ id: request.id }));