@link-assistant/web-capture 1.12.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +2 -1
- package/bin/web-capture.js +5 -7
- package/package.json +32 -18
- package/src/archive.js +1 -2
- package/src/binary-download.js +13 -0
- package/src/docx.js +14 -11
- package/src/fetch.js +1 -2
- package/src/figures.js +2 -12
- package/src/gdocs-images.js +0 -1
- package/src/gdocs.js +0 -1
- package/src/github.js +0 -1
- package/src/lib.js +1 -2
- package/src/localize-images.js +2 -12
- package/src/markdown.js +0 -1
- package/src/shared-dialog.js +0 -1
- package/src/stream.js +0 -1
- package/src/transport.js +23 -5
- package/src/txt.js +0 -1
- package/.changeset/README.md +0 -8
- package/.changeset/config.json +0 -11
- package/.dockerignore +0 -6
- package/.jscpd.json +0 -19
- package/.lenv +0 -8
- package/.prettierignore +0 -8
- package/.prettierrc +0 -10
- package/Dockerfile +0 -52
- package/babel.config.cjs +0 -13
- package/docker-compose.yml +0 -11
- package/eslint.config.js +0 -112
- package/examples/js/capture_cache.js +0 -12
- package/examples/js/engine_comparison.js +0 -39
- package/examples/js/html_download.js +0 -23
- package/examples/js/image_download.js +0 -36
- package/examples/js/markdown_download.js +0 -23
- package/examples/js/playwright_example.js +0 -23
- package/examples/python/html_download.py +0 -15
- package/examples/python/image_download.py +0 -20
- package/examples/python/markdown_download.py +0 -15
- package/examples/python/playwright_example.py +0 -22
- package/jest.config.mjs +0 -20
- package/tests/ci/dependency-freshness.test.js +0 -74
- package/tests/ci/detect-code-changes.test.js +0 -174
- package/tests/ci/docker-install.test.js +0 -31
- package/tests/ci/rust-dockerfile.test.js +0 -54
- package/tests/ci/workflow-policy.test.js +0 -79
- package/tests/e2e/docker.test.js +0 -186
- package/tests/e2e/process.test.js +0 -215
- package/tests/fixtures/bold-coalesce-and-leakage.html +0 -27
- package/tests/fixtures/deepwiki/deepwiki-full-page.png +0 -0
- package/tests/fixtures/deepwiki/index.md +0 -1500
- package/tests/fixtures/deepwiki/part-1.md +0 -776
- package/tests/fixtures/habr-articles.js +0 -45
- package/tests/fixtures/heading-numbering.html +0 -19
- package/tests/fixtures/list-item-with-br.html +0 -8
- package/tests/fixtures/ol-continuous-numbering.html +0 -12
- package/tests/fixtures/paragraph-vs-line-break.html +0 -11
- package/tests/helpers/nock.js +0 -10
- package/tests/helpers/search-fixture.cjs +0 -17
- package/tests/helpers/temporary-directory.js +0 -12
- package/tests/integration/api-endpoints.test.js +0 -418
- package/tests/integration/archive-default-layout.test.js +0 -37
- package/tests/integration/browser-engines.test.js +0 -128
- package/tests/integration/browser-packaged-path.test.js +0 -45
- package/tests/integration/deepwiki-download.test.js +0 -144
- package/tests/integration/deepwiki-rendered-page.mock.test.js +0 -121
- package/tests/integration/formalai-contract.test.js +0 -214
- package/tests/integration/gdocs-public-doc.test.js +0 -315
- package/tests/integration/github-readme.test.js +0 -137
- package/tests/integration/google-drive-image.test.js +0 -246
- package/tests/integration/habr-article.test.js +0 -337
- package/tests/integration/search-endpoint.test.js +0 -88
- package/tests/integration/stackoverflow-download.test.js +0 -173
- package/tests/integration/wikipedia-download.test.js +0 -142
- package/tests/jest.setup.mjs +0 -7
- package/tests/mock/index.test.js +0 -225
- package/tests/unit/animation.test.js +0 -33
- package/tests/unit/batch.test.js +0 -114
- package/tests/unit/bold-coalesce-and-leakage.test.js +0 -54
- package/tests/unit/browser-launch-options.test.js +0 -42
- package/tests/unit/browser.test.js +0 -172
- package/tests/unit/cache.test.js +0 -182
- package/tests/unit/cli.test.js +0 -399
- package/tests/unit/convertGoogleDriveUrl.test.js +0 -89
- package/tests/unit/convertRelativeUrls.test.js +0 -34
- package/tests/unit/extract-images.test.js +0 -259
- package/tests/unit/fetch-html.test.js +0 -54
- package/tests/unit/figures.test.js +0 -74
- package/tests/unit/gdocs-export-headers.test.js +0 -28
- package/tests/unit/gdocs-preprocess.test.js +0 -36
- package/tests/unit/gdocs.test.js +0 -1490
- package/tests/unit/github.test.js +0 -156
- package/tests/unit/heading-image.test.js +0 -49
- package/tests/unit/heading-numbering.test.js +0 -28
- package/tests/unit/html2md-br-in-list-item.test.js +0 -23
- package/tests/unit/html2md-ol-numbering.test.js +0 -20
- package/tests/unit/html2md.test.js +0 -695
- package/tests/unit/image-mode-defaults.test.js +0 -54
- package/tests/unit/kreuzberg.test.js +0 -249
- package/tests/unit/latex.test.js +0 -123
- package/tests/unit/localize-images.test.js +0 -114
- package/tests/unit/markdown.test.js +0 -36
- package/tests/unit/metadata.test.js +0 -140
- package/tests/unit/package-runtime-dependencies.test.js +0 -74
- package/tests/unit/paragraph-vs-line-break.test.js +0 -56
- package/tests/unit/postprocess.test.js +0 -110
- package/tests/unit/proxy-headers.test.js +0 -100
- package/tests/unit/publish-verification.test.js +0 -57
- package/tests/unit/retry.test.js +0 -111
- package/tests/unit/search.test.js +0 -339
- package/tests/unit/shared-dialog.test.js +0 -173
- package/tests/unit/stackoverflow.test.js +0 -32
- package/tests/unit/stream.test.js +0 -85
- package/tests/unit/verify.test.js +0 -182
- package/tests/xpaste/xpaste-screenshot-markdown.mock.test.js +0 -375
- package/tests/xpaste/xpaste.e2e.test.js +0 -36
- package/tests/xpaste/xpaste.integration.test.js +0 -41
- package/tests/xpaste/xpaste.mock.test.js +0 -261
- package/tests/xpaste/xpaste.unit.test.js +0 -119
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import { createBrowser } from '../../src/browser.js';
|
|
2
|
-
|
|
3
|
-
describe('Browser Engine Integration Tests', () => {
|
|
4
|
-
describe('Puppeteer Engine', () => {
|
|
5
|
-
let browser;
|
|
6
|
-
|
|
7
|
-
beforeEach(async () => {
|
|
8
|
-
browser = await createBrowser('puppeteer');
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
afterEach(async () => {
|
|
12
|
-
if (browser) {
|
|
13
|
-
await browser.close();
|
|
14
|
-
}
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it('can navigate to a page and get content', async () => {
|
|
18
|
-
const page = await browser.newPage();
|
|
19
|
-
await page.goto('https://example.com', {
|
|
20
|
-
waitUntil: 'networkidle0',
|
|
21
|
-
timeout: 30000,
|
|
22
|
-
});
|
|
23
|
-
const content = await page.content();
|
|
24
|
-
expect(content).toContain('Example Domain');
|
|
25
|
-
}, 60000);
|
|
26
|
-
|
|
27
|
-
it('can take a screenshot', async () => {
|
|
28
|
-
const page = await browser.newPage();
|
|
29
|
-
await page.setViewport({ width: 1280, height: 800 });
|
|
30
|
-
await page.goto('https://example.com', {
|
|
31
|
-
waitUntil: 'networkidle0',
|
|
32
|
-
timeout: 30000,
|
|
33
|
-
});
|
|
34
|
-
const screenshot = await page.screenshot({ type: 'png' });
|
|
35
|
-
expect(screenshot).toBeInstanceOf(Buffer);
|
|
36
|
-
expect(screenshot.length).toBeGreaterThan(100);
|
|
37
|
-
}, 60000);
|
|
38
|
-
|
|
39
|
-
it('can set custom headers and user agent', async () => {
|
|
40
|
-
const page = await browser.newPage();
|
|
41
|
-
await page.setExtraHTTPHeaders({ 'Accept-Language': 'en-US' });
|
|
42
|
-
await page.setUserAgent('Test User Agent');
|
|
43
|
-
await page.goto('https://example.com', {
|
|
44
|
-
waitUntil: 'networkidle0',
|
|
45
|
-
timeout: 30000,
|
|
46
|
-
});
|
|
47
|
-
const content = await page.content();
|
|
48
|
-
expect(content).toBeTruthy();
|
|
49
|
-
}, 60000);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
describe('Playwright Engine', () => {
|
|
53
|
-
let browser;
|
|
54
|
-
|
|
55
|
-
beforeEach(async () => {
|
|
56
|
-
browser = await createBrowser('playwright');
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
afterEach(async () => {
|
|
60
|
-
if (browser) {
|
|
61
|
-
await browser.close();
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('can navigate to a page and get content', async () => {
|
|
66
|
-
const page = await browser.newPage();
|
|
67
|
-
await page.goto('https://example.com', {
|
|
68
|
-
waitUntil: 'networkidle0',
|
|
69
|
-
timeout: 30000,
|
|
70
|
-
});
|
|
71
|
-
const content = await page.content();
|
|
72
|
-
expect(content).toContain('Example Domain');
|
|
73
|
-
}, 60000);
|
|
74
|
-
|
|
75
|
-
it('can take a screenshot', async () => {
|
|
76
|
-
const page = await browser.newPage();
|
|
77
|
-
await page.setViewport({ width: 1280, height: 800 });
|
|
78
|
-
await page.goto('https://example.com', {
|
|
79
|
-
waitUntil: 'networkidle0',
|
|
80
|
-
timeout: 30000,
|
|
81
|
-
});
|
|
82
|
-
const screenshot = await page.screenshot({ type: 'png' });
|
|
83
|
-
expect(screenshot).toBeInstanceOf(Buffer);
|
|
84
|
-
expect(screenshot.length).toBeGreaterThan(100);
|
|
85
|
-
}, 60000);
|
|
86
|
-
|
|
87
|
-
it('can set custom headers and user agent', async () => {
|
|
88
|
-
const page = await browser.newPage();
|
|
89
|
-
await page.setExtraHTTPHeaders({ 'Accept-Language': 'en-US' });
|
|
90
|
-
await page.setUserAgent('Test User Agent');
|
|
91
|
-
await page.goto('https://example.com', {
|
|
92
|
-
waitUntil: 'networkidle0',
|
|
93
|
-
timeout: 30000,
|
|
94
|
-
});
|
|
95
|
-
const content = await page.content();
|
|
96
|
-
expect(content).toBeTruthy();
|
|
97
|
-
}, 60000);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
describe('Engine Parity', () => {
|
|
101
|
-
it('both engines produce similar content for the same page', async () => {
|
|
102
|
-
const puppeteerBrowser = await createBrowser('puppeteer');
|
|
103
|
-
const playwrightBrowser = await createBrowser('playwright');
|
|
104
|
-
|
|
105
|
-
const puppeteerPage = await puppeteerBrowser.newPage();
|
|
106
|
-
const playwrightPage = await playwrightBrowser.newPage();
|
|
107
|
-
|
|
108
|
-
await puppeteerPage.goto('https://example.com', {
|
|
109
|
-
waitUntil: 'networkidle0',
|
|
110
|
-
timeout: 30000,
|
|
111
|
-
});
|
|
112
|
-
await playwrightPage.goto('https://example.com', {
|
|
113
|
-
waitUntil: 'networkidle0',
|
|
114
|
-
timeout: 30000,
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
const puppeteerContent = await puppeteerPage.content();
|
|
118
|
-
const playwrightContent = await playwrightPage.content();
|
|
119
|
-
|
|
120
|
-
// Both should contain the main content
|
|
121
|
-
expect(puppeteerContent).toContain('Example Domain');
|
|
122
|
-
expect(playwrightContent).toContain('Example Domain');
|
|
123
|
-
|
|
124
|
-
await puppeteerBrowser.close();
|
|
125
|
-
await playwrightBrowser.close();
|
|
126
|
-
}, 60000);
|
|
127
|
-
});
|
|
128
|
-
});
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { mkdtemp, rm, symlink } from 'node:fs/promises';
|
|
2
|
-
import { tmpdir } from 'node:os';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { chromium } from 'playwright';
|
|
5
|
-
import { createBrowser } from '../../src/browser.js';
|
|
6
|
-
|
|
7
|
-
// Only deterministic local HTML is used. Neither engine may select its own cache.
|
|
8
|
-
test.each(['puppeteer', 'playwright'])(
|
|
9
|
-
'%s renders using a Chromium executable at an explicit temporary path',
|
|
10
|
-
async (engine) => {
|
|
11
|
-
const directory = await mkdtemp(path.join(tmpdir(), 'packaged-browser-'));
|
|
12
|
-
const packagedPath = path.join(
|
|
13
|
-
directory,
|
|
14
|
-
process.platform === 'win32' ? 'chrome.exe' : 'chromium'
|
|
15
|
-
);
|
|
16
|
-
let browser;
|
|
17
|
-
let page;
|
|
18
|
-
try {
|
|
19
|
-
await symlink(chromium.executablePath(), packagedPath);
|
|
20
|
-
browser = await createBrowser(engine, {
|
|
21
|
-
executablePath: packagedPath,
|
|
22
|
-
userDataDir: path.join(directory, 'profile'),
|
|
23
|
-
});
|
|
24
|
-
page = await browser.newPage();
|
|
25
|
-
await page.goto(
|
|
26
|
-
'data:text/html,<main id="result"></main><script>document.getElementById("result").textContent="Packaged Chromium rendered"</script>'
|
|
27
|
-
);
|
|
28
|
-
expect(await page.content()).toContain('Packaged Chromium rendered');
|
|
29
|
-
const executable =
|
|
30
|
-
engine === 'puppeteer'
|
|
31
|
-
? browser.rawBrowser.process().spawnfile
|
|
32
|
-
: packagedPath;
|
|
33
|
-
expect(executable).toBe(packagedPath);
|
|
34
|
-
} finally {
|
|
35
|
-
if (page) {
|
|
36
|
-
await page.close();
|
|
37
|
-
}
|
|
38
|
-
if (browser) {
|
|
39
|
-
await browser.close();
|
|
40
|
-
}
|
|
41
|
-
await rm(directory, { recursive: true, force: true });
|
|
42
|
-
}
|
|
43
|
-
},
|
|
44
|
-
30000
|
|
45
|
-
);
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Integration test for converting the DeepWiki search page from issue #17.
|
|
3
|
-
*
|
|
4
|
-
* The page is JavaScript-rendered, so this suite verifies the /markdown
|
|
5
|
-
* endpoint captures rendered content instead of the initial Next.js shell.
|
|
6
|
-
*
|
|
7
|
-
* Set DEEPWIKI_INTEGRATION=true to run it. The suite is skipped by default so
|
|
8
|
-
* local/offline test runs remain deterministic.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { jest } from '@jest/globals';
|
|
12
|
-
import fs from 'fs';
|
|
13
|
-
import path from 'path';
|
|
14
|
-
import request from 'supertest';
|
|
15
|
-
import { fileURLToPath } from 'url';
|
|
16
|
-
import { app } from '../../src/index.js';
|
|
17
|
-
import { retry } from '../../src/retry.js';
|
|
18
|
-
|
|
19
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
20
|
-
const DEEPWIKI_URL =
|
|
21
|
-
'https://deepwiki.com/search/-57-4-23-57_0e4aa687-7a9d-4591-8c6f-67c4b2d732f6';
|
|
22
|
-
const FIXTURE_DIR = path.join(__dirname, '..', 'fixtures', 'deepwiki');
|
|
23
|
-
const INLINE_MARKDOWN_LINE_LIMIT = 1500;
|
|
24
|
-
const PNG_SIGNATURE = Buffer.from([
|
|
25
|
-
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
|
26
|
-
]);
|
|
27
|
-
const ORDERED_CONTENT_MARKERS = [
|
|
28
|
-
'Search | DeepWiki',
|
|
29
|
-
'deep-assistant/hive-mind',
|
|
30
|
-
'расскажи пожалуйста на английском',
|
|
31
|
-
'Deep\n\nThought Process',
|
|
32
|
-
'# Hive Mind: A Comprehensive Overview of an AI Agent System for Software Development',
|
|
33
|
-
'## Executive Summary',
|
|
34
|
-
'## I. Foundational Philosophy: Human-AI Collaboration Model',
|
|
35
|
-
'## II. Architectural Layers: The Three-Tier Design',
|
|
36
|
-
'## III. Original Ideas and Innovations',
|
|
37
|
-
'Innovation 1: The Task Clarification System',
|
|
38
|
-
'Innovation 2: Multi-Dimensional Feedback Detection',
|
|
39
|
-
'Innovation 3: Thinking Depth Control',
|
|
40
|
-
'## IV. The Workflow Logic: How Everything Connects',
|
|
41
|
-
'## XI. Practical Implications: What This Enables',
|
|
42
|
-
];
|
|
43
|
-
|
|
44
|
-
const SKIP_LIVE =
|
|
45
|
-
!process.env.DEEPWIKI_INTEGRATION ||
|
|
46
|
-
process.env.DEEPWIKI_INTEGRATION === 'false';
|
|
47
|
-
const describeIfLive = SKIP_LIVE ? describe.skip : describe;
|
|
48
|
-
|
|
49
|
-
jest.setTimeout(180000);
|
|
50
|
-
|
|
51
|
-
function readFixtureChunks() {
|
|
52
|
-
return ['index.md', 'part-1.md'].map((filename) =>
|
|
53
|
-
fs.readFileSync(path.join(FIXTURE_DIR, filename), 'utf8')
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function splitMarkdown(markdown) {
|
|
58
|
-
const lines = markdown.split('\n');
|
|
59
|
-
const chunks = [];
|
|
60
|
-
for (
|
|
61
|
-
let index = 0;
|
|
62
|
-
index < lines.length;
|
|
63
|
-
index += INLINE_MARKDOWN_LINE_LIMIT
|
|
64
|
-
) {
|
|
65
|
-
chunks.push(
|
|
66
|
-
lines.slice(index, index + INLINE_MARKDOWN_LINE_LIMIT).join('\n')
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
return chunks;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function expectMarkersInOrder(markdown, markers) {
|
|
73
|
-
let previousIndex = -1;
|
|
74
|
-
for (const marker of markers) {
|
|
75
|
-
const index = markdown.indexOf(marker);
|
|
76
|
-
expect(index).toBeGreaterThan(previousIndex);
|
|
77
|
-
previousIndex = index;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
async function getDeepwikiMarkdown() {
|
|
82
|
-
const response = await retry(
|
|
83
|
-
() =>
|
|
84
|
-
request(app)
|
|
85
|
-
.get('/markdown')
|
|
86
|
-
.query({ url: DEEPWIKI_URL, engine: 'playwright' })
|
|
87
|
-
.expect(200),
|
|
88
|
-
{
|
|
89
|
-
retries: 2,
|
|
90
|
-
baseDelay: 3000,
|
|
91
|
-
onRetry: (err, attempt, delay) => {
|
|
92
|
-
console.log(
|
|
93
|
-
`Retry ${attempt} for DeepWiki markdown after ${delay}ms: ${err.message}`
|
|
94
|
-
);
|
|
95
|
-
},
|
|
96
|
-
}
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
expect(response.headers['content-type']).toMatch(/text\/markdown/);
|
|
100
|
-
return response.text;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
describe('DeepWiki committed reference fixtures (issue #17)', () => {
|
|
104
|
-
it('stores markdown split into inline-sized reference documents', () => {
|
|
105
|
-
const fixtureChunks = readFixtureChunks();
|
|
106
|
-
const fixtureMarkdown = fixtureChunks.join('\n');
|
|
107
|
-
|
|
108
|
-
expect(fixtureChunks).toHaveLength(2);
|
|
109
|
-
expect(fixtureChunks[0].split('\n').length).toBeLessThanOrEqual(
|
|
110
|
-
INLINE_MARKDOWN_LINE_LIMIT
|
|
111
|
-
);
|
|
112
|
-
expect(fixtureChunks[1].split('\n').length).toBeGreaterThan(0);
|
|
113
|
-
expect(fixtureMarkdown.split('\n').length).toBeGreaterThan(
|
|
114
|
-
INLINE_MARKDOWN_LINE_LIMIT
|
|
115
|
-
);
|
|
116
|
-
expectMarkersInOrder(fixtureMarkdown, ORDERED_CONTENT_MARKERS);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it('keeps the full-page screenshot fixture as a PNG visual reference', () => {
|
|
120
|
-
const screenshot = fs.readFileSync(
|
|
121
|
-
path.join(FIXTURE_DIR, 'deepwiki-full-page.png')
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
expect(screenshot.subarray(0, PNG_SIGNATURE.length)).toEqual(PNG_SIGNATURE);
|
|
125
|
-
expect(screenshot.length).toBeGreaterThan(100000);
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
describeIfLive('DeepWiki search page markdown capture (issue #17)', () => {
|
|
130
|
-
it('matches the committed markdown fixture split from the rendered page', async () => {
|
|
131
|
-
const markdown = await getDeepwikiMarkdown();
|
|
132
|
-
const fixtureChunks = readFixtureChunks();
|
|
133
|
-
const fixtureMarkdown = fixtureChunks.join('\n');
|
|
134
|
-
|
|
135
|
-
expect(markdown).not.toMatch(/<html/i);
|
|
136
|
-
expect(markdown).not.toContain('__NEXT_DATA__');
|
|
137
|
-
expect(markdown.split('\n').length).toBeGreaterThan(
|
|
138
|
-
INLINE_MARKDOWN_LINE_LIMIT
|
|
139
|
-
);
|
|
140
|
-
expectMarkersInOrder(markdown, ORDERED_CONTENT_MARKERS);
|
|
141
|
-
expectMarkersInOrder(fixtureMarkdown, ORDERED_CONTENT_MARKERS);
|
|
142
|
-
expect(splitMarkdown(markdown)).toEqual(fixtureChunks);
|
|
143
|
-
}, 180000);
|
|
144
|
-
});
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import { jest } from '@jest/globals';
|
|
2
|
-
import nock from '../helpers/nock.js';
|
|
3
|
-
import request from 'supertest';
|
|
4
|
-
import { URL, fileURLToPath } from 'url';
|
|
5
|
-
|
|
6
|
-
const DEEPWIKI_URL =
|
|
7
|
-
'https://deepwiki.com/search/-57-4-23-57_0e4aa687-7a9d-4591-8c6f-67c4b2d732f6';
|
|
8
|
-
const deepwikiShellHtml = `<!doctype html>
|
|
9
|
-
<html lang="en">
|
|
10
|
-
<head>
|
|
11
|
-
<title>Search | DeepWiki</title>
|
|
12
|
-
<script src="/_next/static/chunks/app.js"></script>
|
|
13
|
-
<script>self.__next_f = []; self.__next_f.push([1, "shell"])</script>
|
|
14
|
-
</head>
|
|
15
|
-
<body><div id="__next"></div></body>
|
|
16
|
-
</html>`;
|
|
17
|
-
const renderedDeepwikiHtml = `<!doctype html>
|
|
18
|
-
<html lang="en">
|
|
19
|
-
<head><title>Search | DeepWiki</title></head>
|
|
20
|
-
<body>
|
|
21
|
-
<main>
|
|
22
|
-
<p>deep-assistant/hive-mind</p>
|
|
23
|
-
<p>расскажи пожалуйста на английском</p>
|
|
24
|
-
<h1>Hive Mind: A Comprehensive Overview of an AI Agent System for Software Development</h1>
|
|
25
|
-
<h2>Executive Summary</h2>
|
|
26
|
-
<p>Deep analysis of the issue-solver workflow.</p>
|
|
27
|
-
<h2>Thought Process</h2>
|
|
28
|
-
<pre><code>const clarifyPrompt = \`Task: "\${taskDescription}"\`;
|
|
29
|
-
await solver.run(clarifyPrompt);</code></pre>
|
|
30
|
-
</main>
|
|
31
|
-
</body>
|
|
32
|
-
</html>`;
|
|
33
|
-
|
|
34
|
-
const fakePage = {
|
|
35
|
-
type: 'playwright',
|
|
36
|
-
rawPage: {
|
|
37
|
-
evaluate: jest
|
|
38
|
-
.fn()
|
|
39
|
-
.mockRejectedValueOnce(
|
|
40
|
-
new Error(
|
|
41
|
-
'Execution context was destroyed, most likely because of a navigation'
|
|
42
|
-
)
|
|
43
|
-
)
|
|
44
|
-
.mockResolvedValue(
|
|
45
|
-
renderedDeepwikiHtml.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ')
|
|
46
|
-
.length
|
|
47
|
-
),
|
|
48
|
-
},
|
|
49
|
-
setViewport: jest.fn(async () => {}),
|
|
50
|
-
setExtraHTTPHeaders: jest.fn(async () => {}),
|
|
51
|
-
setUserAgent: jest.fn(async () => {}),
|
|
52
|
-
goto: jest.fn(async () => {}),
|
|
53
|
-
waitForTimeout: jest.fn(
|
|
54
|
-
(ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
55
|
-
),
|
|
56
|
-
content: jest.fn(async () => renderedDeepwikiHtml),
|
|
57
|
-
};
|
|
58
|
-
const fakeBrowser = {
|
|
59
|
-
newPage: jest.fn(async () => fakePage),
|
|
60
|
-
close: jest.fn(async () => {}),
|
|
61
|
-
};
|
|
62
|
-
const createBrowser = jest.fn(async () => fakeBrowser);
|
|
63
|
-
const browserModulePath = fileURLToPath(
|
|
64
|
-
new URL('../../src/browser.js', import.meta.url)
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
jest.unstable_mockModule(browserModulePath, () => ({
|
|
68
|
-
createBrowser,
|
|
69
|
-
getBrowserEngine: (req) => req.query.engine || 'puppeteer',
|
|
70
|
-
}));
|
|
71
|
-
|
|
72
|
-
const { app } = await import('../../src/index.js');
|
|
73
|
-
|
|
74
|
-
describe('DeepWiki rendered markdown endpoint behavior', () => {
|
|
75
|
-
beforeEach(() => {
|
|
76
|
-
nock.cleanAll();
|
|
77
|
-
jest.clearAllMocks();
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
afterEach(() => {
|
|
81
|
-
nock.cleanAll();
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('converts browser-rendered content when the initial fetch is a Next.js shell', async () => {
|
|
85
|
-
nock('https://deepwiki.com')
|
|
86
|
-
.get('/search/-57-4-23-57_0e4aa687-7a9d-4591-8c6f-67c4b2d732f6')
|
|
87
|
-
.reply(200, deepwikiShellHtml);
|
|
88
|
-
|
|
89
|
-
const response = await request(app)
|
|
90
|
-
.get('/markdown')
|
|
91
|
-
.query({ url: DEEPWIKI_URL, engine: 'playwright' })
|
|
92
|
-
.expect(200);
|
|
93
|
-
|
|
94
|
-
expect(createBrowser).toHaveBeenCalledWith(
|
|
95
|
-
'playwright',
|
|
96
|
-
expect.objectContaining({
|
|
97
|
-
args: expect.arrayContaining([
|
|
98
|
-
expect.stringContaining('--user-agent='),
|
|
99
|
-
'--window-size=1280,800',
|
|
100
|
-
'--disable-blink-features=AutomationControlled',
|
|
101
|
-
]),
|
|
102
|
-
})
|
|
103
|
-
);
|
|
104
|
-
expect(fakePage.goto).toHaveBeenCalledWith(
|
|
105
|
-
DEEPWIKI_URL,
|
|
106
|
-
expect.objectContaining({ waitUntil: 'networkidle0' })
|
|
107
|
-
);
|
|
108
|
-
expect(fakeBrowser.close).toHaveBeenCalledTimes(1);
|
|
109
|
-
expect(response.headers['content-type']).toMatch(/text\/markdown/);
|
|
110
|
-
expect(response.text).toContain('Search | DeepWiki');
|
|
111
|
-
expect(response.text).toContain('deep-assistant/hive-mind');
|
|
112
|
-
expect(response.text).toContain('расскажи пожалуйста на английском');
|
|
113
|
-
expect(response.text).toContain(
|
|
114
|
-
'# Hive Mind: A Comprehensive Overview of an AI Agent System for Software Development'
|
|
115
|
-
);
|
|
116
|
-
expect(response.text).toContain('## Thought Process');
|
|
117
|
-
expect(response.text).toContain('```');
|
|
118
|
-
expect(response.text).toContain('const clarifyPrompt');
|
|
119
|
-
expect(response.text).not.toContain('__next_f');
|
|
120
|
-
});
|
|
121
|
-
});
|
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Smoke tests for the FormalAI-facing HTTP contract (issue #135).
|
|
3
|
-
*
|
|
4
|
-
* These tests intentionally assert the stable response shapes documented in
|
|
5
|
-
* docs/formalai-contract.md rather than endpoint internals.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import request from 'supertest';
|
|
9
|
-
import nock from '../helpers/nock.js';
|
|
10
|
-
import unzipper from 'unzipper';
|
|
11
|
-
import { app, SEARCH_PROVIDERS } from '../../src/index.js';
|
|
12
|
-
|
|
13
|
-
const HTML = `<!doctype html>
|
|
14
|
-
<html>
|
|
15
|
-
<head><title>FormalAI fixture</title></head>
|
|
16
|
-
<body><h1>FormalAI Fixture</h1><p>Stable HTTP shape.</p></body>
|
|
17
|
-
</html>`;
|
|
18
|
-
|
|
19
|
-
const PNG = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
20
|
-
|
|
21
|
-
const WIKI_JSON = {
|
|
22
|
-
pages: [
|
|
23
|
-
{
|
|
24
|
-
id: 1,
|
|
25
|
-
key: 'Formal_methods',
|
|
26
|
-
title: 'Formal methods',
|
|
27
|
-
excerpt: 'the study of <b>formal</b> methods',
|
|
28
|
-
description: 'rigorous techniques',
|
|
29
|
-
},
|
|
30
|
-
],
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
function parseBinary(res, callback) {
|
|
34
|
-
const chunks = [];
|
|
35
|
-
res.on('data', (chunk) => chunks.push(chunk));
|
|
36
|
-
res.on('end', () => callback(null, Buffer.concat(chunks)));
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
afterEach(() => {
|
|
40
|
-
nock.cleanAll();
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
describe('FormalAI HTTP contract (#135)', () => {
|
|
44
|
-
it('returns stable text artifact shapes for /html, /txt, and /markdown', async () => {
|
|
45
|
-
nock('https://formalai.example').get('/page').reply(200, HTML, {
|
|
46
|
-
'content-type': 'text/html; charset=utf-8',
|
|
47
|
-
});
|
|
48
|
-
const html = await request(app)
|
|
49
|
-
.get('/html')
|
|
50
|
-
.query({ url: 'https://formalai.example/page' })
|
|
51
|
-
.expect(200)
|
|
52
|
-
.expect('Content-Type', /text\/html/);
|
|
53
|
-
expect(html.text).toContain('<h1>FormalAI Fixture</h1>');
|
|
54
|
-
|
|
55
|
-
nock('https://formalai.example')
|
|
56
|
-
.get('/text')
|
|
57
|
-
.reply(200, 'FormalAI plain text', {
|
|
58
|
-
'content-type': 'text/plain; charset=utf-8',
|
|
59
|
-
});
|
|
60
|
-
const text = await request(app)
|
|
61
|
-
.get('/txt')
|
|
62
|
-
.query({ url: 'https://formalai.example/text' })
|
|
63
|
-
.expect(200)
|
|
64
|
-
.expect('Content-Type', /text\/plain/);
|
|
65
|
-
expect(text.headers['content-disposition']).toContain('.txt');
|
|
66
|
-
expect(text.text).toBe('FormalAI plain text');
|
|
67
|
-
|
|
68
|
-
nock('https://formalai.example').get('/markdown').reply(200, HTML, {
|
|
69
|
-
'content-type': 'text/html; charset=utf-8',
|
|
70
|
-
});
|
|
71
|
-
const markdown = await request(app)
|
|
72
|
-
.get('/markdown')
|
|
73
|
-
.query({ url: 'https://formalai.example/markdown' })
|
|
74
|
-
.expect(200)
|
|
75
|
-
.expect('Content-Type', /text\/markdown/);
|
|
76
|
-
expect(markdown.text).toContain('FormalAI Fixture');
|
|
77
|
-
expect(markdown.text).toContain('Stable HTTP shape.');
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('preserves upstream status and content for /fetch and /stream', async () => {
|
|
81
|
-
nock('https://formalai.example')
|
|
82
|
-
.get('/fetch-source')
|
|
83
|
-
.reply(203, 'fetch body', {
|
|
84
|
-
'content-type': 'text/plain; charset=utf-8',
|
|
85
|
-
});
|
|
86
|
-
const fetched = await request(app)
|
|
87
|
-
.get('/fetch')
|
|
88
|
-
.query({ url: 'https://formalai.example/fetch-source' })
|
|
89
|
-
.expect(203)
|
|
90
|
-
.expect('Content-Type', /text\/plain/);
|
|
91
|
-
expect(fetched.text).toBe('fetch body');
|
|
92
|
-
|
|
93
|
-
nock('https://formalai.example')
|
|
94
|
-
.get('/stream-source')
|
|
95
|
-
.reply(206, 'stream body', {
|
|
96
|
-
'content-type': 'text/plain; charset=utf-8',
|
|
97
|
-
});
|
|
98
|
-
const streamed = await request(app)
|
|
99
|
-
.get('/stream')
|
|
100
|
-
.query({ url: 'https://formalai.example/stream-source' })
|
|
101
|
-
.expect(206)
|
|
102
|
-
.expect('Content-Type', /text\/plain/);
|
|
103
|
-
expect(streamed.text).toBe('stream body');
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('returns stable binary artifact shapes for /image and /archive', async () => {
|
|
107
|
-
nock('https://drive.google.com')
|
|
108
|
-
.get('/uc')
|
|
109
|
-
.query({ export: 'download', id: 'formalaiimage' })
|
|
110
|
-
.reply(200, PNG, { 'content-type': 'image/png' });
|
|
111
|
-
const image = await request(app)
|
|
112
|
-
.get('/image')
|
|
113
|
-
.query({
|
|
114
|
-
url: 'https://drive.google.com/file/d/formalaiimage/view',
|
|
115
|
-
})
|
|
116
|
-
.buffer(true)
|
|
117
|
-
.parse(parseBinary)
|
|
118
|
-
.expect(200)
|
|
119
|
-
.expect('Content-Type', /image\/png/);
|
|
120
|
-
expect(image.headers['content-disposition']).toContain(
|
|
121
|
-
'google-drive-formalaiimage.png'
|
|
122
|
-
);
|
|
123
|
-
expect(image.body.subarray(0, 8)).toEqual(PNG);
|
|
124
|
-
|
|
125
|
-
nock('https://formalai.example').get('/archive').reply(200, HTML, {
|
|
126
|
-
'content-type': 'text/html; charset=utf-8',
|
|
127
|
-
});
|
|
128
|
-
const archive = await request(app)
|
|
129
|
-
.get('/archive')
|
|
130
|
-
.query({
|
|
131
|
-
url: 'https://formalai.example/archive',
|
|
132
|
-
localImages: 'false',
|
|
133
|
-
})
|
|
134
|
-
.buffer(true)
|
|
135
|
-
.parse(parseBinary)
|
|
136
|
-
.expect(200)
|
|
137
|
-
.expect('Content-Type', /application\/zip/);
|
|
138
|
-
expect(archive.body[0]).toBe(0x50);
|
|
139
|
-
expect(archive.body[1]).toBe(0x4b);
|
|
140
|
-
const zip = await unzipper.Open.buffer(archive.body);
|
|
141
|
-
expect(zip.files.map((file) => file.path)).toEqual(
|
|
142
|
-
expect.arrayContaining(['document.md', 'document.html'])
|
|
143
|
-
);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it('returns normalized /search JSON diagnostics and the documented provider catalog', async () => {
|
|
147
|
-
expect(SEARCH_PROVIDERS).toEqual([
|
|
148
|
-
'wikipedia',
|
|
149
|
-
'duckduckgo',
|
|
150
|
-
'google',
|
|
151
|
-
'bing',
|
|
152
|
-
'brave',
|
|
153
|
-
]);
|
|
154
|
-
|
|
155
|
-
nock('https://en.wikipedia.org')
|
|
156
|
-
.get('/w/rest.php/v1/search/page')
|
|
157
|
-
.query(true)
|
|
158
|
-
.reply(200, WIKI_JSON);
|
|
159
|
-
|
|
160
|
-
const res = await request(app)
|
|
161
|
-
.get('/search')
|
|
162
|
-
.query({ q: 'formal-ai', provider: 'wikipedia', limit: 1 })
|
|
163
|
-
.expect(200)
|
|
164
|
-
.expect('Content-Type', /application\/json/);
|
|
165
|
-
|
|
166
|
-
expect(res.body).toMatchObject({
|
|
167
|
-
query: 'formal-ai',
|
|
168
|
-
provider: 'wikipedia',
|
|
169
|
-
captureMode: 'fetch',
|
|
170
|
-
results: [
|
|
171
|
-
{
|
|
172
|
-
rank: 1,
|
|
173
|
-
title: 'Formal methods',
|
|
174
|
-
url: 'https://en.wikipedia.org/wiki/Formal_methods',
|
|
175
|
-
snippet: 'the study of formal methods',
|
|
176
|
-
},
|
|
177
|
-
],
|
|
178
|
-
diagnostics: {
|
|
179
|
-
status: 200,
|
|
180
|
-
blockedByCors: false,
|
|
181
|
-
blockedByCaptcha: false,
|
|
182
|
-
},
|
|
183
|
-
});
|
|
184
|
-
expect(res.body.capturedAt).toEqual(expect.any(String));
|
|
185
|
-
expect(res.body.diagnostics.sourceUrl).toContain('en.wikipedia.org');
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it('keeps normalized /search diagnostics when provider capture fails', async () => {
|
|
189
|
-
nock('https://en.wikipedia.org')
|
|
190
|
-
.get('/w/rest.php/v1/search/page')
|
|
191
|
-
.query(true)
|
|
192
|
-
.replyWithError(new Error('provider offline'));
|
|
193
|
-
|
|
194
|
-
const res = await request(app)
|
|
195
|
-
.get('/search')
|
|
196
|
-
.query({ q: 'formal-ai', provider: 'wikipedia', limit: 1 })
|
|
197
|
-
.expect(200)
|
|
198
|
-
.expect('Content-Type', /application\/json/);
|
|
199
|
-
|
|
200
|
-
expect(res.body).toMatchObject({
|
|
201
|
-
query: 'formal-ai',
|
|
202
|
-
provider: 'wikipedia',
|
|
203
|
-
captureMode: 'fetch',
|
|
204
|
-
results: [],
|
|
205
|
-
diagnostics: {
|
|
206
|
-
status: 0,
|
|
207
|
-
blockedByCors: false,
|
|
208
|
-
blockedByCaptcha: false,
|
|
209
|
-
},
|
|
210
|
-
});
|
|
211
|
-
expect(res.body.diagnostics.sourceUrl).toContain('en.wikipedia.org');
|
|
212
|
-
expect(res.body.diagnostics.error).toContain('provider offline');
|
|
213
|
-
});
|
|
214
|
-
});
|