@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.
Files changed (119) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +2 -1
  3. package/bin/web-capture.js +5 -7
  4. package/package.json +32 -18
  5. package/src/archive.js +1 -2
  6. package/src/binary-download.js +13 -0
  7. package/src/docx.js +14 -11
  8. package/src/fetch.js +1 -2
  9. package/src/figures.js +2 -12
  10. package/src/gdocs-images.js +0 -1
  11. package/src/gdocs.js +0 -1
  12. package/src/github.js +0 -1
  13. package/src/lib.js +1 -2
  14. package/src/localize-images.js +2 -12
  15. package/src/markdown.js +0 -1
  16. package/src/shared-dialog.js +0 -1
  17. package/src/stream.js +0 -1
  18. package/src/transport.js +23 -5
  19. package/src/txt.js +0 -1
  20. package/.changeset/README.md +0 -8
  21. package/.changeset/config.json +0 -11
  22. package/.dockerignore +0 -6
  23. package/.jscpd.json +0 -19
  24. package/.lenv +0 -8
  25. package/.prettierignore +0 -8
  26. package/.prettierrc +0 -10
  27. package/Dockerfile +0 -52
  28. package/babel.config.cjs +0 -13
  29. package/docker-compose.yml +0 -11
  30. package/eslint.config.js +0 -112
  31. package/examples/js/capture_cache.js +0 -12
  32. package/examples/js/engine_comparison.js +0 -39
  33. package/examples/js/html_download.js +0 -23
  34. package/examples/js/image_download.js +0 -36
  35. package/examples/js/markdown_download.js +0 -23
  36. package/examples/js/playwright_example.js +0 -23
  37. package/examples/python/html_download.py +0 -15
  38. package/examples/python/image_download.py +0 -20
  39. package/examples/python/markdown_download.py +0 -15
  40. package/examples/python/playwright_example.py +0 -22
  41. package/jest.config.mjs +0 -20
  42. package/tests/ci/dependency-freshness.test.js +0 -74
  43. package/tests/ci/detect-code-changes.test.js +0 -174
  44. package/tests/ci/docker-install.test.js +0 -31
  45. package/tests/ci/rust-dockerfile.test.js +0 -54
  46. package/tests/ci/workflow-policy.test.js +0 -79
  47. package/tests/e2e/docker.test.js +0 -186
  48. package/tests/e2e/process.test.js +0 -215
  49. package/tests/fixtures/bold-coalesce-and-leakage.html +0 -27
  50. package/tests/fixtures/deepwiki/deepwiki-full-page.png +0 -0
  51. package/tests/fixtures/deepwiki/index.md +0 -1500
  52. package/tests/fixtures/deepwiki/part-1.md +0 -776
  53. package/tests/fixtures/habr-articles.js +0 -45
  54. package/tests/fixtures/heading-numbering.html +0 -19
  55. package/tests/fixtures/list-item-with-br.html +0 -8
  56. package/tests/fixtures/ol-continuous-numbering.html +0 -12
  57. package/tests/fixtures/paragraph-vs-line-break.html +0 -11
  58. package/tests/helpers/nock.js +0 -10
  59. package/tests/helpers/search-fixture.cjs +0 -17
  60. package/tests/helpers/temporary-directory.js +0 -12
  61. package/tests/integration/api-endpoints.test.js +0 -418
  62. package/tests/integration/archive-default-layout.test.js +0 -37
  63. package/tests/integration/browser-engines.test.js +0 -128
  64. package/tests/integration/browser-packaged-path.test.js +0 -45
  65. package/tests/integration/deepwiki-download.test.js +0 -144
  66. package/tests/integration/deepwiki-rendered-page.mock.test.js +0 -121
  67. package/tests/integration/formalai-contract.test.js +0 -214
  68. package/tests/integration/gdocs-public-doc.test.js +0 -315
  69. package/tests/integration/github-readme.test.js +0 -137
  70. package/tests/integration/google-drive-image.test.js +0 -246
  71. package/tests/integration/habr-article.test.js +0 -337
  72. package/tests/integration/search-endpoint.test.js +0 -88
  73. package/tests/integration/stackoverflow-download.test.js +0 -173
  74. package/tests/integration/wikipedia-download.test.js +0 -142
  75. package/tests/jest.setup.mjs +0 -7
  76. package/tests/mock/index.test.js +0 -225
  77. package/tests/unit/animation.test.js +0 -33
  78. package/tests/unit/batch.test.js +0 -114
  79. package/tests/unit/bold-coalesce-and-leakage.test.js +0 -54
  80. package/tests/unit/browser-launch-options.test.js +0 -42
  81. package/tests/unit/browser.test.js +0 -172
  82. package/tests/unit/cache.test.js +0 -182
  83. package/tests/unit/cli.test.js +0 -399
  84. package/tests/unit/convertGoogleDriveUrl.test.js +0 -89
  85. package/tests/unit/convertRelativeUrls.test.js +0 -34
  86. package/tests/unit/extract-images.test.js +0 -259
  87. package/tests/unit/fetch-html.test.js +0 -54
  88. package/tests/unit/figures.test.js +0 -74
  89. package/tests/unit/gdocs-export-headers.test.js +0 -28
  90. package/tests/unit/gdocs-preprocess.test.js +0 -36
  91. package/tests/unit/gdocs.test.js +0 -1490
  92. package/tests/unit/github.test.js +0 -156
  93. package/tests/unit/heading-image.test.js +0 -49
  94. package/tests/unit/heading-numbering.test.js +0 -28
  95. package/tests/unit/html2md-br-in-list-item.test.js +0 -23
  96. package/tests/unit/html2md-ol-numbering.test.js +0 -20
  97. package/tests/unit/html2md.test.js +0 -695
  98. package/tests/unit/image-mode-defaults.test.js +0 -54
  99. package/tests/unit/kreuzberg.test.js +0 -249
  100. package/tests/unit/latex.test.js +0 -123
  101. package/tests/unit/localize-images.test.js +0 -114
  102. package/tests/unit/markdown.test.js +0 -36
  103. package/tests/unit/metadata.test.js +0 -140
  104. package/tests/unit/package-runtime-dependencies.test.js +0 -74
  105. package/tests/unit/paragraph-vs-line-break.test.js +0 -56
  106. package/tests/unit/postprocess.test.js +0 -110
  107. package/tests/unit/proxy-headers.test.js +0 -100
  108. package/tests/unit/publish-verification.test.js +0 -57
  109. package/tests/unit/retry.test.js +0 -111
  110. package/tests/unit/search.test.js +0 -339
  111. package/tests/unit/shared-dialog.test.js +0 -173
  112. package/tests/unit/stackoverflow.test.js +0 -32
  113. package/tests/unit/stream.test.js +0 -85
  114. package/tests/unit/verify.test.js +0 -182
  115. package/tests/xpaste/xpaste-screenshot-markdown.mock.test.js +0 -375
  116. package/tests/xpaste/xpaste.e2e.test.js +0 -36
  117. package/tests/xpaste/xpaste.integration.test.js +0 -41
  118. package/tests/xpaste/xpaste.mock.test.js +0 -261
  119. package/tests/xpaste/xpaste.unit.test.js +0 -119
@@ -1,339 +0,0 @@
1
- /**
2
- * Unit tests for the structured search-provider capture module (issue #130).
3
- *
4
- * Parsers are pure functions tested against fixtures; the orchestrating
5
- * `search()` is exercised with an injected fetch so no network is required.
6
- */
7
- import { jest } from '@jest/globals';
8
-
9
- import {
10
- search,
11
- parseSearchResults,
12
- buildSearchUrl,
13
- looksLikeCaptcha,
14
- formatSearchAsMarkdown,
15
- SEARCH_PROVIDERS,
16
- DEFAULT_PROVIDER,
17
- } from '../../src/search.js';
18
-
19
- const WIKI_JSON = JSON.stringify({
20
- pages: [
21
- {
22
- id: 1,
23
- key: 'Formal_methods',
24
- title: 'Formal methods',
25
- excerpt: 'the <span class="searchmatch">study</span> of <b>formal</b>',
26
- description: 'mathematically rigorous techniques',
27
- },
28
- {
29
- id: 2,
30
- key: 'Formal_system',
31
- title: 'Formal system',
32
- excerpt: 'an abstract structure',
33
- description: '',
34
- },
35
- ],
36
- });
37
-
38
- const DDG_HTML = `
39
- <div class="result__body">
40
- <a class="result__a" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fexample.com%2Fa&amp;rut=abc">First &amp; Best</a>
41
- <div class="result__snippet">Snippet about the <b>first</b> result</div>
42
- </div>
43
- <div class="result__body">
44
- <a class="result__a" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fexample.org%2Fb">Second result</a>
45
- <div class="result__snippet">Snippet two</div>
46
- </div>
47
- `;
48
-
49
- const BING_HTML = `
50
- <ol id="b_results">
51
- <li class="b_algo">
52
- <h2><a href="https://bing-result.example/1">Bing One</a></h2>
53
- <div class="b_caption"><p>Bing snippet one</p></div>
54
- </li>
55
- </ol>
56
- `;
57
-
58
- describe('search module (#130)', () => {
59
- describe('buildSearchUrl', () => {
60
- it('builds the Wikipedia REST API url with limit', () => {
61
- expect(buildSearchUrl('wikipedia', 'formal ai', 5)).toBe(
62
- 'https://en.wikipedia.org/w/rest.php/v1/search/page?q=formal%20ai&limit=5'
63
- );
64
- });
65
-
66
- it('builds the DuckDuckGo HTML url', () => {
67
- expect(buildSearchUrl('duckduckgo', 'a b')).toContain(
68
- 'https://html.duckduckgo.com/html/?q=a%20b'
69
- );
70
- });
71
-
72
- it('throws for an unknown provider', () => {
73
- expect(() => buildSearchUrl('yahoo', 'x')).toThrow(
74
- /Unknown search provider/
75
- );
76
- });
77
- });
78
-
79
- it('keeps URL construction and response parsing transport-independent', () => {
80
- const sourceUrl = buildSearchUrl('wikipedia', 'formal ai', 1);
81
- const { results } = parseSearchResults('wikipedia', WIKI_JSON, {
82
- limit: 1,
83
- });
84
-
85
- expect(sourceUrl).toBe(
86
- 'https://en.wikipedia.org/w/rest.php/v1/search/page?q=formal%20ai&limit=1'
87
- );
88
- expect(results).toEqual([
89
- {
90
- rank: 1,
91
- title: 'Formal methods',
92
- url: 'https://en.wikipedia.org/wiki/Formal_methods',
93
- snippet: 'the study of formal',
94
- },
95
- ]);
96
- });
97
-
98
- describe('parseSearchResults', () => {
99
- it('normalizes Wikipedia REST JSON and strips markup', () => {
100
- const { results } = parseSearchResults('wikipedia', WIKI_JSON, {
101
- limit: 10,
102
- });
103
- expect(results).toHaveLength(2);
104
- expect(results[0]).toEqual({
105
- rank: 1,
106
- title: 'Formal methods',
107
- url: 'https://en.wikipedia.org/wiki/Formal_methods',
108
- snippet: 'the study of formal',
109
- });
110
- expect(results[1].url).toBe(
111
- 'https://en.wikipedia.org/wiki/Formal_system'
112
- );
113
- });
114
-
115
- it('respects the limit', () => {
116
- const { results } = parseSearchResults('wikipedia', WIKI_JSON, {
117
- limit: 1,
118
- });
119
- expect(results).toHaveLength(1);
120
- });
121
-
122
- it('decodes DuckDuckGo redirect hrefs and entities', () => {
123
- const { results } = parseSearchResults('duckduckgo', DDG_HTML, {
124
- limit: 10,
125
- });
126
- expect(results).toHaveLength(2);
127
- expect(results[0]).toEqual({
128
- rank: 1,
129
- title: 'First & Best',
130
- url: 'https://example.com/a',
131
- snippet: 'Snippet about the first result',
132
- });
133
- expect(results[1].url).toBe('https://example.org/b');
134
- });
135
-
136
- it('parses Bing algorithmic results', () => {
137
- const { results } = parseSearchResults('bing', BING_HTML, { limit: 10 });
138
- expect(results).toEqual([
139
- {
140
- rank: 1,
141
- title: 'Bing One',
142
- url: 'https://bing-result.example/1',
143
- snippet: 'Bing snippet one',
144
- },
145
- ]);
146
- });
147
-
148
- it('returns empty results for malformed JSON', () => {
149
- const { results } = parseSearchResults('wikipedia', 'not json', {
150
- limit: 10,
151
- });
152
- expect(results).toEqual([]);
153
- });
154
-
155
- it('flags CAPTCHA pages', () => {
156
- const { blockedByCaptcha } = parseSearchResults(
157
- 'google',
158
- '<html>Our systems have detected unusual traffic</html>',
159
- { limit: 10 }
160
- );
161
- expect(blockedByCaptcha).toBe(true);
162
- });
163
- });
164
-
165
- describe('looksLikeCaptcha', () => {
166
- it('detects common bot-block phrases', () => {
167
- expect(looksLikeCaptcha('Please solve the CAPTCHA')).toBe(true);
168
- expect(looksLikeCaptcha('normal results page')).toBe(false);
169
- expect(looksLikeCaptcha('')).toBe(false);
170
- });
171
- });
172
-
173
- describe('search()', () => {
174
- const fixedNow = () => '2026-05-30T00:00:00.000Z';
175
-
176
- it('returns the normalized contract for a provider', async () => {
177
- const fetchImpl = async (url) => {
178
- expect(url).toContain('en.wikipedia.org');
179
- return { status: 200, text: async () => WIKI_JSON };
180
- };
181
- const result = await search({
182
- query: 'formal-ai',
183
- provider: 'wikipedia',
184
- limit: 2,
185
- fetchImpl,
186
- now: fixedNow,
187
- });
188
- expect(result).toMatchObject({
189
- query: 'formal-ai',
190
- provider: 'wikipedia',
191
- captureMode: 'fetch',
192
- capturedAt: '2026-05-30T00:00:00.000Z',
193
- });
194
- expect(result.results).toHaveLength(2);
195
- expect(result.diagnostics).toEqual({
196
- status: 200,
197
- blockedByCors: false,
198
- blockedByCaptcha: false,
199
- sourceUrl:
200
- 'https://en.wikipedia.org/w/rest.php/v1/search/page?q=formal-ai&limit=2',
201
- });
202
- });
203
-
204
- it('defaults to the Wikipedia provider', async () => {
205
- const fetchImpl = async () => ({
206
- status: 200,
207
- text: async () => WIKI_JSON,
208
- });
209
- const result = await search({ query: 'x', fetchImpl, now: fixedNow });
210
- expect(result.provider).toBe(DEFAULT_PROVIDER);
211
- expect(DEFAULT_PROVIDER).toBe('wikipedia');
212
- });
213
-
214
- it('records transport failures in diagnostics without throwing', async () => {
215
- const fetchImpl = async () => {
216
- throw new Error('network down');
217
- };
218
- const result = await search({
219
- query: 'x',
220
- provider: 'google',
221
- fetchImpl,
222
- now: fixedNow,
223
- });
224
- expect(result.results).toEqual([]);
225
- expect(result.diagnostics.error).toBe('network down');
226
- });
227
-
228
- it('returns the source receipt and forwards caller cancellation', async () => {
229
- const body = Buffer.from(WIKI_JSON);
230
- const controller = new globalThis.AbortController();
231
- const transport = jest.fn(async ({ url, signal }) => ({
232
- body,
233
- finalUrl: `${url}&redirected=true`,
234
- status: 200,
235
- headers: { 'content-type': 'application/json', etag: 'search-v1' },
236
- diagnostics: { outcome: 'response' },
237
- signal,
238
- }));
239
-
240
- const result = await search({
241
- query: 'formal-ai',
242
- transport,
243
- signal: controller.signal,
244
- now: fixedNow,
245
- });
246
-
247
- expect(Buffer.from(result.receipt.body)).toEqual(body);
248
- expect(result.receipt.finalUrl).toContain('redirected=true');
249
- expect(result.receipt.headers.etag).toBe('search-v1');
250
- expect(transport).toHaveBeenCalledWith(
251
- expect.objectContaining({ signal: controller.signal })
252
- );
253
- });
254
-
255
- it('classifies AbortSignal cancellation in structured diagnostics', async () => {
256
- const controller = new globalThis.AbortController();
257
- controller.abort();
258
- const transport = async ({ signal }) => {
259
- expect(signal).toBe(controller.signal);
260
- const error = new Error('cancelled by caller');
261
- error.name = 'AbortError';
262
- throw error;
263
- };
264
-
265
- const result = await search({
266
- query: 'formal-ai',
267
- transport,
268
- signal: controller.signal,
269
- now: fixedNow,
270
- });
271
-
272
- expect(result.diagnostics).toMatchObject({
273
- status: 0,
274
- errorKind: 'cancelled',
275
- error: 'cancelled by caller',
276
- });
277
- });
278
-
279
- it('rejects an empty query', async () => {
280
- await expect(search({ query: ' ' })).rejects.toThrow(/Missing .query/);
281
- });
282
-
283
- it('rejects an unknown provider', async () => {
284
- await expect(search({ query: 'x', provider: 'yahoo' })).rejects.toThrow(
285
- /Unknown search provider/
286
- );
287
- });
288
- });
289
-
290
- describe('formatSearchAsMarkdown', () => {
291
- it('renders a markdown document with ranked links', () => {
292
- const md = formatSearchAsMarkdown({
293
- query: 'formal-ai',
294
- provider: 'wikipedia',
295
- captureMode: 'fetch',
296
- capturedAt: '2026-05-30T00:00:00.000Z',
297
- results: [
298
- {
299
- rank: 1,
300
- title: 'Formal methods',
301
- url: 'https://en.wikipedia.org/wiki/Formal_methods',
302
- snippet: 'study of formal',
303
- },
304
- ],
305
- diagnostics: {
306
- sourceUrl: 'https://example.com',
307
- blockedByCaptcha: false,
308
- },
309
- });
310
- expect(md).toContain('# Search results for "formal-ai"');
311
- expect(md).toContain(
312
- '1. [Formal methods](https://en.wikipedia.org/wiki/Formal_methods)'
313
- );
314
- expect(md).toContain('study of formal');
315
- });
316
-
317
- it('reports no results', () => {
318
- const md = formatSearchAsMarkdown({
319
- query: 'nothing',
320
- provider: 'bing',
321
- captureMode: 'fetch',
322
- capturedAt: 't',
323
- results: [],
324
- diagnostics: { sourceUrl: 'u', blockedByCaptcha: false },
325
- });
326
- expect(md).toContain('_No results._');
327
- });
328
- });
329
-
330
- it('exposes the documented provider list', () => {
331
- expect(SEARCH_PROVIDERS).toEqual([
332
- 'wikipedia',
333
- 'duckduckgo',
334
- 'google',
335
- 'bing',
336
- 'brave',
337
- ]);
338
- });
339
- });
@@ -1,173 +0,0 @@
1
- import { readFileSync } from 'node:fs';
2
- import { dirname, resolve } from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
-
5
- import { jest } from '@jest/globals';
6
-
7
- import {
8
- captureSharedDialog,
9
- formatSharedDialogAsDemoMemory,
10
- formatSharedDialogAsMarkdown,
11
- formatSharedDialogAsMetaLanguage,
12
- parseSharedDialog,
13
- } from '../../src/shared-dialog.js';
14
-
15
- const __filename = fileURLToPath(import.meta.url);
16
- const __dirname = dirname(__filename);
17
- const repoRoot = resolve(__dirname, '../../..');
18
-
19
- const CHATGPT_SHARE_URL =
20
- 'https://chatgpt.com/share/6a3825b9-8de4-83ee-9c24-52fd1eb38d24';
21
- const GOOGLE_AI_MODE_URL = 'https://share.google/aimode/VG0HhpnAXrBkC0QgP';
22
-
23
- function readCaseStudyFixture(name) {
24
- return readFileSync(
25
- resolve(repoRoot, 'docs/case-studies/issue-141/raw-data', name),
26
- 'utf-8'
27
- );
28
- }
29
-
30
- describe('shared-dialog module (#141)', () => {
31
- it('extracts the four visible ChatGPT shared-dialog turns', () => {
32
- const capture = parseSharedDialog(
33
- readCaseStudyFixture('chatgpt-share-6a3825b9.html'),
34
- {
35
- sourceUrl: CHATGPT_SHARE_URL,
36
- captureMethod: 'static_http',
37
- }
38
- );
39
-
40
- expect(capture).toMatchObject({
41
- provider: 'chatgpt',
42
- sourceUrl: CHATGPT_SHARE_URL,
43
- captureMethod: 'static_http',
44
- status: 'ok',
45
- conversationId: '6a3825b9-8de4-83ee-9c24-52fd1eb38d24',
46
- title: 'Infinite loop script',
47
- });
48
- expect(capture.turns).toHaveLength(4);
49
- expect(capture.turns.map((turn) => turn.role)).toEqual([
50
- 'user',
51
- 'assistant',
52
- 'user',
53
- 'assistant',
54
- ]);
55
- expect(capture.turns[0]).toMatchObject({
56
- visibility: 'visible',
57
- sourceEvidence: [
58
- {
59
- sourceUrl: CHATGPT_SHARE_URL,
60
- kind: 'chatgpt_linear_conversation',
61
- },
62
- ],
63
- });
64
- expect(capture.turns[0].content).toContain('make a loop of that');
65
- expect(capture.turns[1].content).toContain(
66
- 'while true; do sleep 30m && hive-cleanup -f; done'
67
- );
68
- expect(capture.turns[3].content).toContain(
69
- 'screen -dmS auto-cleanup bash -c'
70
- );
71
- });
72
-
73
- it('formats ChatGPT captures as demo_memory', () => {
74
- const capture = parseSharedDialog(
75
- readCaseStudyFixture('chatgpt-share-6a3825b9.html'),
76
- {
77
- sourceUrl: CHATGPT_SHARE_URL,
78
- captureMethod: 'static_http',
79
- }
80
- );
81
-
82
- const memory = formatSharedDialogAsDemoMemory(capture, {
83
- demoLabel: 'issue-552-chatgpt-share',
84
- });
85
-
86
- expect(memory).toContain('demo_memory');
87
- expect(memory.match(/\n {2}event "/g)).toHaveLength(4);
88
- expect(memory).toContain('role "user"');
89
- expect(memory).toContain('role "assistant"');
90
- expect(memory).toContain(
91
- 'conversationId "6a3825b9-8de4-83ee-9c24-52fd1eb38d24"'
92
- );
93
- expect(memory).toContain('conversationTitle "Infinite loop script"');
94
- expect(memory).toContain(`evidence "${CHATGPT_SHARE_URL}"`);
95
- expect(memory).toContain(
96
- "screen -dmS auto-cleanup bash -c 'while true; do sleep 30m && hive-cleanup -f; done'"
97
- );
98
- });
99
-
100
- it('formats captures as shared-dialog meta-language and Markdown', () => {
101
- const capture = parseSharedDialog(
102
- readCaseStudyFixture('chatgpt-share-6a3825b9.html'),
103
- {
104
- sourceUrl: CHATGPT_SHARE_URL,
105
- captureMethod: 'static_http',
106
- }
107
- );
108
-
109
- const metaLanguage = formatSharedDialogAsMetaLanguage(capture);
110
- expect(metaLanguage).toContain('shared_dialog_capture');
111
- expect(metaLanguage).toContain('provider "chatgpt"');
112
- expect(metaLanguage).toContain(
113
- 'turn "0c9f0151-b5a1-402f-afc3-6bd34a0d01d2"'
114
- );
115
-
116
- const markdown = formatSharedDialogAsMarkdown(capture);
117
- expect(markdown).toContain('# Infinite loop script');
118
- expect(markdown).toContain('**User**');
119
- expect(markdown).toContain('**Assistant**');
120
- });
121
-
122
- it('returns a structured Google AI Mode interstitial diagnostic', () => {
123
- const capture = parseSharedDialog(
124
- readCaseStudyFixture('google-ai-mode-VG0HhpnAXrBkC0QgP.html'),
125
- {
126
- sourceUrl: GOOGLE_AI_MODE_URL,
127
- captureMethod: 'static_http',
128
- }
129
- );
130
-
131
- expect(capture).toMatchObject({
132
- provider: 'google_ai_mode',
133
- sourceUrl: GOOGLE_AI_MODE_URL,
134
- captureMethod: 'static_http',
135
- status: 'unsupported',
136
- turns: [],
137
- diagnostics: {
138
- unsupportedReason: 'provider_challenge_interstitial',
139
- },
140
- });
141
- });
142
-
143
- it('uses browser-rendered DOM when static Google AI Mode capture is blocked', async () => {
144
- const googleHtml = readCaseStudyFixture(
145
- 'google-ai-mode-VG0HhpnAXrBkC0QgP.html'
146
- );
147
- const renderHtml = jest.fn(async () => googleHtml);
148
- const capture = await captureSharedDialog({
149
- url: GOOGLE_AI_MODE_URL,
150
- capture: 'browser',
151
- fetchImpl: async () => ({
152
- ok: true,
153
- status: 200,
154
- text: async () => googleHtml,
155
- }),
156
- renderHtml,
157
- now: () => '2026-06-25T00:00:00.000Z',
158
- });
159
-
160
- expect(renderHtml).toHaveBeenCalledWith(GOOGLE_AI_MODE_URL, {
161
- engine: 'puppeteer',
162
- });
163
- expect(capture).toMatchObject({
164
- provider: 'google_ai_mode',
165
- captureMethod: 'browser',
166
- status: 'unsupported',
167
- capturedAt: '2026-06-25T00:00:00.000Z',
168
- diagnostics: {
169
- unsupportedReason: 'provider_challenge_interstitial',
170
- },
171
- });
172
- });
173
- });
@@ -1,32 +0,0 @@
1
- import {
2
- isStackOverflowQuestionUrl,
3
- stackPrinterUrlForQuestion,
4
- } from '../../src/lib.js';
5
-
6
- describe('Stack Overflow URL helpers', () => {
7
- it('recognizes Stack Overflow question URLs', () => {
8
- expect(
9
- isStackOverflowQuestionUrl(
10
- 'https://stackoverflow.com/questions/927358/how-do-i-undo-the-most-recent-local-commits-in-git'
11
- )
12
- ).toBe(true);
13
- expect(
14
- isStackOverflowQuestionUrl(
15
- 'https://stackoverflow.com/questions/tagged/git'
16
- )
17
- ).toBe(false);
18
- expect(
19
- isStackOverflowQuestionUrl('https://serverfault.com/questions/927358')
20
- ).toBe(false);
21
- });
22
-
23
- it('builds a StackPrinter URL for direct captures', () => {
24
- const stackPrinterUrl = stackPrinterUrlForQuestion(
25
- 'https://stackoverflow.com/questions/927358/how-do-i-undo-the-most-recent-local-commits-in-git'
26
- );
27
-
28
- expect(stackPrinterUrl).toBe(
29
- 'https://stackprinter.appspot.com/export?question=927358&service=stackoverflow&language=en&hideAnswers=false&showAll=true&width=640'
30
- );
31
- });
32
- });
@@ -1,85 +0,0 @@
1
- import express from 'express';
2
- import http from 'http';
3
- import request from 'supertest';
4
- import { streamHandler } from '../../src/stream.js';
5
-
6
- describe('streamHandler', () => {
7
- it('streams upstream content with proxy-safe framing', async () => {
8
- const body = '<!DOCTYPE html><html><body>Example Domain</body></html>';
9
- let upstreamHeaders;
10
- const { server, url } = await startUpstreamServer((req, res) => {
11
- upstreamHeaders = req.headers;
12
- res.writeHead(200, {
13
- 'Content-Type': 'text/html; charset=utf-8',
14
- 'Content-Length': Buffer.byteLength(body),
15
- });
16
- res.end(body);
17
- });
18
-
19
- try {
20
- const res = await request(testApp())
21
- .get('/stream')
22
- .query({ url })
23
- .expect(200);
24
-
25
- expect(upstreamHeaders['accept-encoding']).toBe('identity');
26
- expect(res.headers['content-type']).toBe('text/html; charset=utf-8');
27
- expect(res.headers['content-length']).toBe(
28
- String(Buffer.byteLength(body))
29
- );
30
- expect(res.text).toContain('Example Domain');
31
- } finally {
32
- await closeServer(server);
33
- }
34
- });
35
-
36
- it('ends the downstream response when upstream closes after body bytes', async () => {
37
- const body = '<!DOCTYPE html><html><body>Example Domain</body></html>';
38
- const { server, url } = await startUpstreamServer((req, res) => {
39
- res.writeHead(200, {
40
- 'Content-Type': 'text/html; charset=utf-8',
41
- });
42
- res.write(body);
43
- res.socket.end();
44
- });
45
-
46
- try {
47
- const res = await request(testApp())
48
- .get('/stream')
49
- .query({ url })
50
- .expect(200);
51
-
52
- expect(res.text).toContain('Example Domain');
53
- } finally {
54
- await closeServer(server);
55
- }
56
- });
57
- });
58
-
59
- function testApp() {
60
- const app = express();
61
- app.get('/stream', streamHandler);
62
- return app;
63
- }
64
-
65
- async function startUpstreamServer(handler) {
66
- const server = http.createServer(handler);
67
- await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
68
- const { port } = server.address();
69
- return {
70
- server,
71
- url: `http://127.0.0.1:${port}`,
72
- };
73
- }
74
-
75
- async function closeServer(server) {
76
- await new Promise((resolve, reject) => {
77
- server.close((error) => {
78
- if (error) {
79
- reject(error);
80
- } else {
81
- resolve();
82
- }
83
- });
84
- });
85
- }