@ever-works/jina-plugin 1.0.0 → 1.0.3

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.
@@ -1,318 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { JinaReaderPlugin } from '../jina.plugin.js';
3
- import type { PluginContext, SearchOptions, ContentExtractionOptions } from '@ever-works/plugin';
4
-
5
- const buildContext = (settings: Record<string, unknown> = {}): PluginContext =>
6
- ({
7
- pluginId: 'jina',
8
- logger: {
9
- log: vi.fn(),
10
- debug: vi.fn(),
11
- warn: vi.fn(),
12
- error: vi.fn()
13
- },
14
- getSettings: vi.fn().mockResolvedValue(settings)
15
- }) as unknown as PluginContext;
16
-
17
- const okResponse = (body: unknown): Response =>
18
- ({
19
- ok: true,
20
- status: 200,
21
- statusText: 'OK',
22
- json: () => Promise.resolve(body),
23
- text: () => Promise.resolve(JSON.stringify(body))
24
- }) as unknown as Response;
25
-
26
- const errResponse = (status: number, statusText = 'error'): Response =>
27
- ({
28
- ok: false,
29
- status,
30
- statusText,
31
- json: () => Promise.resolve({}),
32
- text: () => Promise.resolve(statusText)
33
- }) as unknown as Response;
34
-
35
- describe('JinaReaderPlugin', () => {
36
- let plugin: JinaReaderPlugin;
37
- let fetchMock: ReturnType<typeof vi.fn>;
38
- const originalFetch = globalThis.fetch;
39
-
40
- beforeEach(() => {
41
- plugin = new JinaReaderPlugin();
42
- fetchMock = vi.fn();
43
- globalThis.fetch = fetchMock as unknown as typeof fetch;
44
- });
45
-
46
- afterEach(() => {
47
- globalThis.fetch = originalFetch;
48
- vi.clearAllMocks();
49
- });
50
-
51
- describe('metadata', () => {
52
- it('exposes stable identity fields', () => {
53
- expect(plugin.id).toBe('jina');
54
- expect(plugin.name).toBe('Jina AI');
55
- expect(plugin.version).toBe('1.0.0');
56
- expect(plugin.category).toBe('content-extractor');
57
- expect(plugin.providerName).toBe('Jina');
58
- });
59
-
60
- it('declares both search and content-extractor capabilities', () => {
61
- expect(plugin.capabilities).toEqual(['search', 'content-extractor']);
62
- });
63
- });
64
-
65
- describe('settingsSchema', () => {
66
- it('requires apiKey and marks it as user-scoped secret', () => {
67
- expect(plugin.settingsSchema.required).toContain('apiKey');
68
- const props = plugin.settingsSchema.properties as Record<string, Record<string, unknown>>;
69
- expect(props.apiKey['x-secret']).toBe(true);
70
- expect(props.apiKey['x-envVar']).toBe('PLUGIN_JINA_API_KEY');
71
- });
72
- });
73
-
74
- describe('search', () => {
75
- const opts = (overrides: Partial<SearchOptions> = {}): SearchOptions => ({
76
- query: 'hello',
77
- settings: { apiKey: 'k' },
78
- ...overrides
79
- });
80
-
81
- it('returns mapped results with derived hostname source', async () => {
82
- fetchMock.mockResolvedValueOnce(
83
- okResponse({
84
- data: [
85
- {
86
- title: 'Example',
87
- url: 'https://www.example.com/post',
88
- description: 'snippet here',
89
- date: '2026-01-01'
90
- }
91
- ]
92
- })
93
- );
94
-
95
- const r = await plugin.search(opts({ limit: 5, region: 'us', language: 'en' }));
96
-
97
- expect(r.results).toHaveLength(1);
98
- expect(r.results[0]).toMatchObject({
99
- title: 'Example',
100
- url: 'https://www.example.com/post',
101
- snippet: 'snippet here',
102
- position: 1,
103
- publishedDate: '2026-01-01',
104
- source: 'www.example.com'
105
- });
106
-
107
- const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
108
- expect(url).toBe('https://s.jina.ai/');
109
- expect(init.method).toBe('POST');
110
- expect((init.headers as Record<string, string>).Authorization).toBe('Bearer k');
111
- const body = JSON.parse(init.body as string);
112
- expect(body.q).toBe('hello');
113
- expect(body.num).toBe(5);
114
- expect(body.gl).toBe('us');
115
- expect(body.hl).toBe('en');
116
- });
117
-
118
- it('forwards X-Site header for the first includeDomain', async () => {
119
- fetchMock.mockResolvedValueOnce(okResponse({ data: [] }));
120
- await plugin.search(opts({ includeDomains: ['a.com', 'b.com'] }));
121
- const init = fetchMock.mock.calls[0][1] as RequestInit;
122
- expect((init.headers as Record<string, string>)['X-Site']).toBe('a.com');
123
- });
124
-
125
- it('handles missing data array safely', async () => {
126
- fetchMock.mockResolvedValueOnce(okResponse({}));
127
- const r = await plugin.search(opts());
128
- expect(r.results).toEqual([]);
129
- expect(r.totalResults).toBe(0);
130
- });
131
-
132
- it('throws and logs on non-OK upstream', async () => {
133
- await plugin.onLoad(buildContext());
134
- fetchMock.mockResolvedValueOnce(errResponse(503, 'Service Unavailable'));
135
- await expect(plugin.search(opts())).rejects.toThrow(/Jina Search API returned 503/);
136
- });
137
- });
138
-
139
- describe('isAvailable', () => {
140
- it('returns false without context', async () => {
141
- expect(await plugin.isAvailable()).toBe(false);
142
- });
143
-
144
- it('reflects whether settings has an apiKey', async () => {
145
- await plugin.onLoad(buildContext({ apiKey: 'k' }));
146
- expect(await plugin.isAvailable()).toBe(true);
147
-
148
- await plugin.onLoad(buildContext({}));
149
- expect(await plugin.isAvailable()).toBe(false);
150
- });
151
- });
152
-
153
- describe('validateConnection', () => {
154
- it('fails fast without apiKey', async () => {
155
- const r = await plugin.validateConnection({});
156
- expect(r.success).toBe(false);
157
- expect(r.message).toMatch(/not configured/i);
158
- expect(fetchMock).not.toHaveBeenCalled();
159
- });
160
-
161
- it('returns success on a 200 response', async () => {
162
- fetchMock.mockResolvedValueOnce(okResponse({ data: [] }));
163
- const r = await plugin.validateConnection({ apiKey: 'k' });
164
- expect(r.success).toBe(true);
165
- });
166
-
167
- it('returns failure on a non-OK response', async () => {
168
- fetchMock.mockResolvedValueOnce(errResponse(401, 'Unauthorized'));
169
- const r = await plugin.validateConnection({ apiKey: 'k' });
170
- expect(r.success).toBe(false);
171
- expect(r.message).toMatch(/401/);
172
- });
173
-
174
- it('returns failure when fetch throws', async () => {
175
- fetchMock.mockRejectedValueOnce(new Error('boom'));
176
- const r = await plugin.validateConnection({ apiKey: 'k' });
177
- expect(r.success).toBe(false);
178
- expect(r.message).toMatch(/boom/);
179
- });
180
- });
181
-
182
- describe('extract', () => {
183
- const opts = (overrides: Partial<ContentExtractionOptions> = {}): ContentExtractionOptions => ({
184
- url: 'https://example.com/post',
185
- settings: { apiKey: 'k' },
186
- ...overrides
187
- });
188
-
189
- it('returns success with title, content, images, links, metadata, wordCount, readingTime', async () => {
190
- fetchMock.mockResolvedValueOnce(
191
- okResponse({
192
- data: {
193
- title: 'Title',
194
- url: 'https://example.com/post',
195
- content: 'one two three four five',
196
- description: 'desc',
197
- publishedTime: '2026-01-01',
198
- images: { 'alt one': 'https://img/1.png' },
199
- links: { 'link one': 'https://target.example' },
200
- metadata: { lang: 'en' }
201
- }
202
- })
203
- );
204
-
205
- const r = await plugin.extract(opts());
206
- expect(r.success).toBe(true);
207
- expect(r.title).toBe('Title');
208
- expect(r.markdown).toBe('one two three four five');
209
- expect(r.wordCount).toBe(5);
210
- expect(r.readingTime).toBe(1);
211
- expect(r.images).toHaveLength(1);
212
- expect(r.images?.[0]).toMatchObject({ src: 'https://img/1.png', alt: 'alt one' });
213
- expect(r.links?.[0]).toMatchObject({ href: 'https://target.example', text: 'link one' });
214
- expect(r.metadata).toMatchObject({
215
- description: 'desc',
216
- publishedDate: '2026-01-01',
217
- language: 'en'
218
- });
219
- });
220
-
221
- it('exposes finalUrl when SDK returns a different URL', async () => {
222
- fetchMock.mockResolvedValueOnce(
223
- okResponse({
224
- data: {
225
- title: 't',
226
- url: 'https://example.com/redirected',
227
- content: 'hi'
228
- }
229
- })
230
- );
231
- const r = await plugin.extract(opts());
232
- expect(r.finalUrl).toBe('https://example.com/redirected');
233
- });
234
-
235
- it('returns failure when content is missing', async () => {
236
- fetchMock.mockResolvedValueOnce(okResponse({ data: { title: 't', url: 'u' } }));
237
- const r = await plugin.extract(opts());
238
- expect(r.success).toBe(false);
239
- expect(r.error).toMatch(/No content/);
240
- });
241
-
242
- it('returns failure when upstream returns non-OK', async () => {
243
- fetchMock.mockResolvedValueOnce(errResponse(429, 'Too Many Requests'));
244
- const r = await plugin.extract(opts());
245
- expect(r.success).toBe(false);
246
- expect(r.error).toMatch(/429/);
247
- });
248
-
249
- it('omits images when includeImages is false', async () => {
250
- fetchMock.mockResolvedValueOnce(
251
- okResponse({
252
- data: {
253
- title: 't',
254
- url: 'u',
255
- content: 'hi',
256
- images: { foo: 'https://i' }
257
- }
258
- })
259
- );
260
- const r = await plugin.extract(opts({ includeImages: false }));
261
- expect(r.images).toBeUndefined();
262
- });
263
- });
264
-
265
- describe('extractBatch', () => {
266
- it('chunks URLs into batches of 5', async () => {
267
- const urls = Array.from({ length: 7 }, (_, i) => `https://example.com/${i}`);
268
- for (const u of urls) {
269
- fetchMock.mockResolvedValueOnce(okResponse({ data: { title: 't', url: u, content: 'a b' } }));
270
- }
271
- const r = await plugin.extractBatch(urls, { settings: { apiKey: 'k' } });
272
- expect(r).toHaveLength(7);
273
- expect(r.every((x) => x.success === true)).toBe(true);
274
- expect(fetchMock).toHaveBeenCalledTimes(7);
275
- });
276
- });
277
-
278
- describe('canExtract / getSupportedFormats', () => {
279
- it('accepts http and https URLs', async () => {
280
- expect(await plugin.canExtract('https://example.com')).toBe(true);
281
- expect(await plugin.canExtract('http://example.com')).toBe(true);
282
- });
283
-
284
- it('rejects non-http schemes and invalid URLs', async () => {
285
- expect(await plugin.canExtract('ftp://example.com')).toBe(false);
286
- expect(await plugin.canExtract('garbage')).toBe(false);
287
- });
288
-
289
- it('exposes text + markdown formats', () => {
290
- expect(plugin.getSupportedFormats()).toEqual(['text', 'markdown']);
291
- });
292
- });
293
-
294
- describe('lifecycle', () => {
295
- it('logs on load and clears context on unload', async () => {
296
- const ctx = buildContext();
297
- await plugin.onLoad(ctx);
298
- expect(ctx.logger.log).toHaveBeenCalledWith('Jina AI Reader Plugin loaded');
299
- await plugin.onUnload();
300
- expect(await plugin.isAvailable()).toBe(false);
301
- });
302
- });
303
-
304
- describe('healthCheck + manifest', () => {
305
- it('reports healthy', async () => {
306
- const h = await plugin.healthCheck();
307
- expect(h.status).toBe('healthy');
308
- });
309
-
310
- it('returns a manifest aligned with plugin metadata', () => {
311
- const m = plugin.getManifest();
312
- expect(m.id).toBe('jina');
313
- expect(m.category).toBe('content-extractor');
314
- expect(m.capabilities).toEqual(['search', 'content-extractor']);
315
- expect(m.builtIn).toBe(true);
316
- });
317
- });
318
- });
package/src/index.ts DELETED
@@ -1,2 +0,0 @@
1
- export { JinaReaderPlugin } from './jina.plugin.js';
2
- export { JinaReaderPlugin as default } from './jina.plugin.js';