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.
- package/.claude/hooks/post-edit-check.sh +5 -3
- package/.claude/skills/atomic-commits/SKILL.md +3 -1
- package/.husky/pre-commit +3 -2
- package/.prettierrc +9 -0
- package/.versionrc.json +1 -1
- package/CHANGELOG.md +70 -0
- package/CLAUDE.md +6 -0
- package/README.md +25 -13
- package/bun.lock +277 -33
- package/dist/{chunk-L2YVNC63.js → chunk-6FHWC36B.js} +9 -1
- package/dist/chunk-6FHWC36B.js.map +1 -0
- package/dist/{chunk-RST4XGRL.js → chunk-DC7CGSGT.js} +288 -241
- package/dist/chunk-DC7CGSGT.js.map +1 -0
- package/dist/{chunk-6PBP5DVD.js → chunk-WFNPNAAP.js} +3212 -3054
- package/dist/chunk-WFNPNAAP.js.map +1 -0
- package/dist/{chunk-WT2DAEO7.js → chunk-Z2KKVH45.js} +548 -482
- package/dist/chunk-Z2KKVH45.js.map +1 -0
- package/dist/index.js +871 -758
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +3 -3
- package/dist/watch.service-BJV3TI3F.js +7 -0
- package/dist/workers/background-worker-cli.js +97 -71
- package/dist/workers/background-worker-cli.js.map +1 -1
- package/eslint.config.js +43 -1
- package/package.json +18 -11
- package/plugin.json +8 -0
- package/python/requirements.txt +1 -1
- package/src/analysis/ast-parser.test.ts +12 -11
- package/src/analysis/ast-parser.ts +28 -22
- package/src/analysis/code-graph.test.ts +52 -62
- package/src/analysis/code-graph.ts +9 -13
- package/src/analysis/dependency-usage-analyzer.test.ts +91 -271
- package/src/analysis/dependency-usage-analyzer.ts +52 -24
- package/src/analysis/go-ast-parser.test.ts +22 -22
- package/src/analysis/go-ast-parser.ts +18 -25
- package/src/analysis/parser-factory.test.ts +9 -9
- package/src/analysis/parser-factory.ts +3 -3
- package/src/analysis/python-ast-parser.test.ts +27 -27
- package/src/analysis/python-ast-parser.ts +2 -2
- package/src/analysis/repo-url-resolver.test.ts +82 -82
- package/src/analysis/rust-ast-parser.test.ts +19 -19
- package/src/analysis/rust-ast-parser.ts +17 -27
- package/src/analysis/tree-sitter-parser.test.ts +3 -3
- package/src/analysis/tree-sitter-parser.ts +10 -16
- package/src/cli/commands/crawl.test.ts +40 -24
- package/src/cli/commands/crawl.ts +186 -166
- package/src/cli/commands/index-cmd.test.ts +90 -90
- package/src/cli/commands/index-cmd.ts +52 -36
- package/src/cli/commands/mcp.test.ts +6 -6
- package/src/cli/commands/mcp.ts +2 -2
- package/src/cli/commands/plugin-api.test.ts +16 -18
- package/src/cli/commands/plugin-api.ts +9 -6
- package/src/cli/commands/search.test.ts +16 -7
- package/src/cli/commands/search.ts +124 -87
- package/src/cli/commands/serve.test.ts +67 -25
- package/src/cli/commands/serve.ts +18 -3
- package/src/cli/commands/setup.test.ts +176 -101
- package/src/cli/commands/setup.ts +140 -117
- package/src/cli/commands/store.test.ts +82 -53
- package/src/cli/commands/store.ts +56 -37
- package/src/cli/program.ts +2 -2
- package/src/crawl/article-converter.test.ts +4 -1
- package/src/crawl/article-converter.ts +46 -31
- package/src/crawl/bridge.test.ts +240 -132
- package/src/crawl/bridge.ts +87 -30
- package/src/crawl/claude-client.test.ts +124 -56
- package/src/crawl/claude-client.ts +7 -15
- package/src/crawl/intelligent-crawler.test.ts +65 -22
- package/src/crawl/intelligent-crawler.ts +86 -53
- package/src/crawl/markdown-utils.ts +1 -4
- package/src/db/embeddings.ts +4 -6
- package/src/db/lance.test.ts +4 -4
- package/src/db/lance.ts +16 -12
- package/src/index.ts +26 -17
- package/src/logging/index.ts +1 -5
- package/src/logging/logger.ts +3 -5
- package/src/logging/payload.test.ts +1 -1
- package/src/logging/payload.ts +3 -5
- package/src/mcp/commands/index.ts +2 -2
- package/src/mcp/commands/job.commands.ts +12 -18
- package/src/mcp/commands/meta.commands.ts +13 -13
- package/src/mcp/commands/registry.ts +5 -8
- package/src/mcp/commands/store.commands.ts +19 -19
- package/src/mcp/handlers/execute.handler.test.ts +10 -10
- package/src/mcp/handlers/execute.handler.ts +4 -5
- package/src/mcp/handlers/index.ts +10 -14
- package/src/mcp/handlers/job.handler.test.ts +10 -10
- package/src/mcp/handlers/job.handler.ts +22 -25
- package/src/mcp/handlers/search.handler.test.ts +36 -65
- package/src/mcp/handlers/search.handler.ts +135 -104
- package/src/mcp/handlers/store.handler.test.ts +41 -52
- package/src/mcp/handlers/store.handler.ts +108 -88
- package/src/mcp/schemas/index.test.ts +73 -68
- package/src/mcp/schemas/index.ts +18 -12
- package/src/mcp/server.test.ts +1 -1
- package/src/mcp/server.ts +59 -46
- package/src/plugin/commands.test.ts +230 -95
- package/src/plugin/commands.ts +24 -25
- package/src/plugin/dependency-analyzer.test.ts +52 -52
- package/src/plugin/dependency-analyzer.ts +85 -22
- package/src/plugin/git-clone.test.ts +24 -13
- package/src/plugin/git-clone.ts +3 -7
- package/src/server/app.test.ts +109 -109
- package/src/server/app.ts +32 -23
- package/src/server/index.test.ts +64 -66
- package/src/services/chunking.service.test.ts +32 -32
- package/src/services/chunking.service.ts +16 -9
- package/src/services/code-graph.service.test.ts +30 -36
- package/src/services/code-graph.service.ts +24 -10
- package/src/services/code-unit.service.test.ts +55 -11
- package/src/services/code-unit.service.ts +85 -11
- package/src/services/config.service.test.ts +37 -18
- package/src/services/config.service.ts +30 -7
- package/src/services/index.service.test.ts +49 -18
- package/src/services/index.service.ts +98 -48
- package/src/services/index.ts +6 -9
- package/src/services/job.service.test.ts +22 -22
- package/src/services/job.service.ts +18 -18
- package/src/services/project-root.service.test.ts +1 -3
- package/src/services/search.service.test.ts +248 -120
- package/src/services/search.service.ts +286 -156
- package/src/services/services.test.ts +1 -1
- package/src/services/snippet.service.test.ts +14 -6
- package/src/services/snippet.service.ts +7 -5
- package/src/services/store.service.test.ts +68 -29
- package/src/services/store.service.ts +41 -12
- package/src/services/watch.service.test.ts +34 -14
- package/src/services/watch.service.ts +11 -1
- package/src/types/brands.test.ts +3 -1
- package/src/types/index.ts +2 -13
- package/src/types/search.ts +10 -8
- package/src/utils/type-guards.test.ts +20 -15
- package/src/utils/type-guards.ts +1 -1
- package/src/workers/background-worker-cli.ts +28 -30
- package/src/workers/background-worker.test.ts +54 -40
- package/src/workers/background-worker.ts +76 -60
- package/src/workers/pid-file.test.ts +167 -0
- package/src/workers/pid-file.ts +82 -0
- package/src/workers/spawn-worker.test.ts +22 -10
- package/src/workers/spawn-worker.ts +6 -6
- package/tests/analysis/ast-parser.test.ts +3 -3
- package/tests/analysis/code-graph.test.ts +5 -5
- package/tests/fixtures/code-snippets/api/error-handling.ts +4 -15
- package/tests/fixtures/code-snippets/api/rest-controller.ts +3 -9
- package/tests/fixtures/code-snippets/auth/jwt-auth.ts +5 -21
- package/tests/fixtures/code-snippets/auth/oauth-flow.ts +4 -4
- package/tests/fixtures/code-snippets/database/repository-pattern.ts +11 -3
- package/tests/fixtures/corpus/oss-repos/hono/src/adapter/aws-lambda/handler.ts +2 -2
- package/tests/fixtures/corpus/oss-repos/hono/src/adapter/cloudflare-pages/handler.ts +1 -1
- package/tests/fixtures/corpus/oss-repos/hono/src/adapter/cloudflare-workers/serve-static.ts +2 -2
- package/tests/fixtures/corpus/oss-repos/hono/src/client/client.ts +2 -2
- package/tests/fixtures/corpus/oss-repos/hono/src/client/types.ts +22 -20
- package/tests/fixtures/corpus/oss-repos/hono/src/context.ts +13 -10
- package/tests/fixtures/corpus/oss-repos/hono/src/helper/accepts/accepts.ts +10 -7
- package/tests/fixtures/corpus/oss-repos/hono/src/helper/adapter/index.ts +2 -2
- package/tests/fixtures/corpus/oss-repos/hono/src/helper/css/index.ts +1 -1
- package/tests/fixtures/corpus/oss-repos/hono/src/helper/factory/index.ts +16 -16
- package/tests/fixtures/corpus/oss-repos/hono/src/helper/ssg/ssg.ts +2 -2
- package/tests/fixtures/corpus/oss-repos/hono/src/hono-base.ts +3 -3
- package/tests/fixtures/corpus/oss-repos/hono/src/hono.ts +1 -1
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/dom/css.ts +2 -2
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/dom/intrinsic-element/components.ts +1 -1
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/dom/render.ts +7 -7
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/hooks/index.ts +3 -3
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/intrinsic-element/components.ts +1 -1
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/utils.ts +6 -6
- package/tests/fixtures/corpus/oss-repos/hono/src/middleware/jsx-renderer/index.ts +3 -3
- package/tests/fixtures/corpus/oss-repos/hono/src/middleware/serve-static/index.ts +1 -1
- package/tests/fixtures/corpus/oss-repos/hono/src/preset/quick.ts +1 -1
- package/tests/fixtures/corpus/oss-repos/hono/src/preset/tiny.ts +1 -1
- package/tests/fixtures/corpus/oss-repos/hono/src/router/pattern-router/router.ts +2 -2
- package/tests/fixtures/corpus/oss-repos/hono/src/router/reg-exp-router/node.ts +4 -4
- package/tests/fixtures/corpus/oss-repos/hono/src/router/reg-exp-router/router.ts +1 -1
- package/tests/fixtures/corpus/oss-repos/hono/src/router/trie-router/node.ts +1 -1
- package/tests/fixtures/corpus/oss-repos/hono/src/types.ts +166 -169
- package/tests/fixtures/corpus/oss-repos/hono/src/utils/body.ts +8 -8
- package/tests/fixtures/corpus/oss-repos/hono/src/utils/color.ts +3 -3
- package/tests/fixtures/corpus/oss-repos/hono/src/utils/cookie.ts +2 -2
- package/tests/fixtures/corpus/oss-repos/hono/src/utils/encode.ts +2 -2
- package/tests/fixtures/corpus/oss-repos/hono/src/utils/types.ts +30 -33
- package/tests/fixtures/corpus/oss-repos/hono/src/validator/validator.ts +2 -2
- package/tests/fixtures/test-server.ts +3 -2
- package/tests/helpers/performance-metrics.ts +8 -25
- package/tests/helpers/search-relevance.ts +14 -69
- package/tests/integration/cli-consistency.test.ts +6 -5
- package/tests/integration/python-bridge.test.ts +13 -3
- package/tests/mcp/server.test.ts +1 -1
- package/tests/services/code-unit.service.test.ts +48 -0
- package/tests/services/job.service.test.ts +124 -0
- package/tests/services/search.progressive-context.test.ts +2 -2
- package/.claude-plugin/plugin.json +0 -13
- package/dist/chunk-6PBP5DVD.js.map +0 -1
- package/dist/chunk-L2YVNC63.js.map +0 -1
- package/dist/chunk-RST4XGRL.js.map +0 -1
- package/dist/chunk-WT2DAEO7.js.map +0 -1
- package/dist/watch.service-YAIKKDCF.js +0 -7
- package/skills/atomic-commits/SKILL.md +0 -77
- /package/dist/{watch.service-YAIKKDCF.js.map → watch.service-BJV3TI3F.js.map} +0 -0
package/src/crawl/bridge.test.ts
CHANGED
|
@@ -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
|
|
51
|
-
|
|
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
|
|
65
|
+
return mockStderrReadline.asInterface();
|
|
56
66
|
}
|
|
57
|
-
return mockReadline
|
|
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(
|
|
114
|
-
|
|
115
|
-
|
|
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(
|
|
131
|
-
|
|
132
|
-
|
|
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(
|
|
151
|
-
|
|
152
|
-
|
|
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(
|
|
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(
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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(
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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(
|
|
231
|
-
|
|
232
|
-
|
|
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(
|
|
255
|
-
|
|
256
|
-
|
|
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(
|
|
268
|
-
|
|
269
|
-
|
|
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(
|
|
284
|
-
|
|
285
|
-
|
|
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(
|
|
301
|
-
|
|
302
|
-
|
|
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(
|
|
315
|
-
|
|
316
|
-
|
|
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(
|
|
337
|
-
|
|
338
|
-
|
|
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
|
|
458
|
+
vi.mocked(spawn).mockReturnValue(newMockProcess.asChildProcess());
|
|
398
459
|
vi.mocked(createInterface)
|
|
399
|
-
.mockReturnValueOnce(newMockStderrReadline
|
|
400
|
-
.mockReturnValueOnce(newMockReadline
|
|
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(
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
437
|
-
|
|
438
|
-
|
|
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(
|
|
446
|
-
|
|
447
|
-
|
|
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(
|
|
462
|
-
|
|
463
|
-
|
|
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(
|
|
471
|
-
|
|
472
|
-
|
|
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(
|
|
487
|
-
|
|
488
|
-
|
|
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
|
|
501
|
-
await
|
|
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
|
|
666
|
+
vi.mocked(spawn).mockReturnValue(newMockProcess.asChildProcess());
|
|
568
667
|
vi.mocked(createInterface)
|
|
569
|
-
.mockReturnValueOnce(newMockStderrReadline
|
|
570
|
-
.mockReturnValueOnce(newMockReadline
|
|
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
|
|
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
|
|
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
|
|
714
|
+
vi.mocked(spawn).mockReturnValue(newMockProcess.asChildProcess());
|
|
613
715
|
vi.mocked(createInterface)
|
|
614
|
-
.mockReturnValueOnce(newMockStderrReadline
|
|
615
|
-
.mockReturnValueOnce(newMockReadline
|
|
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
|
|
736
|
+
vi.mocked(spawn).mockReturnValue(newMockProcess.asChildProcess());
|
|
635
737
|
vi.mocked(createInterface)
|
|
636
|
-
.mockReturnValueOnce(newMockStderrReadline
|
|
637
|
-
.mockReturnValueOnce(newMockReadline
|
|
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(
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
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
|
-
|
|
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 }));
|