@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,140 +0,0 @@
1
- import * as cheerio from 'cheerio';
2
- import {
3
- extractMetadata,
4
- formatMetadataBlock,
5
- formatFooterBlock,
6
- } from '../../src/metadata.js';
7
-
8
- describe('metadata module', () => {
9
- describe('extractMetadata', () => {
10
- it('extracts author information', () => {
11
- const $ = cheerio.load(
12
- '<a class="tm-user-info__username" href="/user/john">john_doe</a>'
13
- );
14
- const meta = extractMetadata($);
15
- expect(meta.author).toBe('john_doe');
16
- expect(meta.authorUrl).toBe('/user/john');
17
- });
18
-
19
- it('extracts publication date', () => {
20
- const $ = cheerio.load(
21
- '<time datetime="2024-01-15T10:00:00Z">Jan 15</time>'
22
- );
23
- const meta = extractMetadata($);
24
- expect(meta.publishDate).toBe('2024-01-15T10:00:00Z');
25
- expect(meta.publishDateText).toBe('Jan 15');
26
- });
27
-
28
- it('extracts hubs', () => {
29
- const $ = cheerio.load(`
30
- <a class="tm-publication-hub__link"><span>Math</span></a>
31
- <a class="tm-publication-hub__link"><span>Science</span></a>
32
- `);
33
- const meta = extractMetadata($);
34
- expect(meta.hubs).toEqual(['Math', 'Science']);
35
- });
36
-
37
- it('extracts tags from meta keywords', () => {
38
- const $ = cheerio.load(
39
- '<meta name="keywords" content="math, science, theory">'
40
- );
41
- const meta = extractMetadata($);
42
- expect(meta.tags).toEqual(['math', 'science', 'theory']);
43
- });
44
-
45
- it('extracts LD+JSON author name', () => {
46
- const $ = cheerio.load(
47
- '<script type="application/ld+json">{"author":{"name":"John Doe"},"dateModified":"2024-02-01"}</script>'
48
- );
49
- const meta = extractMetadata($);
50
- expect(meta.authorFullName).toBe('John Doe');
51
- expect(meta.dateModified).toBe('2024-02-01');
52
- });
53
-
54
- it('extracts votes and comments', () => {
55
- const $ = cheerio.load(`
56
- <span class="tm-votes-meter__value">+42</span>
57
- <span class="tm-article-comments-counter-link__value">15</span>
58
- `);
59
- const meta = extractMetadata($);
60
- expect(meta.votes).toBe('+42');
61
- expect(meta.comments).toBe('15');
62
- });
63
-
64
- it('returns empty object for minimal HTML', () => {
65
- const $ = cheerio.load('<div>plain content</div>');
66
- const meta = extractMetadata($);
67
- expect(meta).toBeDefined();
68
- expect(meta.author).toBeUndefined();
69
- });
70
- });
71
-
72
- describe('formatMetadataBlock', () => {
73
- it('formats author line with link', () => {
74
- const lines = formatMetadataBlock({
75
- author: 'john',
76
- authorUrl: '/user/john',
77
- });
78
- expect(lines).toContain('**Author:** [john](/user/john)');
79
- });
80
-
81
- it('formats author with full name', () => {
82
- const lines = formatMetadataBlock({
83
- author: 'john',
84
- authorFullName: 'John Doe',
85
- authorUrl: '/user/john',
86
- });
87
- expect(lines[0]).toContain('John Doe (john)');
88
- });
89
-
90
- it('formats publication date', () => {
91
- const lines = formatMetadataBlock({
92
- publishDate: '2024-01-15T10:00:00Z',
93
- });
94
- expect(lines.some((l) => l.includes('Published:'))).toBe(true);
95
- expect(lines.some((l) => l.includes('January 15, 2024'))).toBe(true);
96
- });
97
-
98
- it('includes hubs and tags', () => {
99
- const lines = formatMetadataBlock({
100
- hubs: ['Math', 'Science'],
101
- tags: ['theory', 'links'],
102
- });
103
- expect(lines.some((l) => l.includes('Math, Science'))).toBe(true);
104
- expect(lines.some((l) => l.includes('theory, links'))).toBe(true);
105
- });
106
-
107
- it('returns empty array for null metadata', () => {
108
- expect(formatMetadataBlock(null)).toEqual([]);
109
- });
110
- });
111
-
112
- describe('formatFooterBlock', () => {
113
- it('includes separator line', () => {
114
- const lines = formatFooterBlock({ author: 'test' });
115
- expect(lines[0]).toBe('---');
116
- });
117
-
118
- it('formats tag links', () => {
119
- const lines = formatFooterBlock({
120
- tagLinks: [{ name: 'math', url: '/tag/math' }],
121
- });
122
- expect(lines.some((l) => l.includes('[math](/tag/math)'))).toBe(true);
123
- });
124
-
125
- it('formats article stats', () => {
126
- const lines = formatFooterBlock({
127
- votes: '+10',
128
- views: '5000',
129
- comments: '3',
130
- });
131
- const statsLine = lines.find((l) => l.includes('Votes:'));
132
- expect(statsLine).toContain('+10');
133
- expect(statsLine).toContain('5000');
134
- });
135
-
136
- it('returns empty array for null metadata', () => {
137
- expect(formatFooterBlock(null)).toEqual([]);
138
- });
139
- });
140
- });
@@ -1,74 +0,0 @@
1
- import { readFileSync } from 'fs';
2
- import { dirname, resolve } from 'path';
3
- import { fileURLToPath } from 'url';
4
-
5
- const __filename = fileURLToPath(import.meta.url);
6
- const __dirname = dirname(__filename);
7
- const jsRoot = resolve(__dirname, '../..');
8
-
9
- function readJson(relativePath) {
10
- return JSON.parse(readFileSync(resolve(jsRoot, relativePath), 'utf8'));
11
- }
12
-
13
- function collectProductionDependencies(packageLock, packageName) {
14
- const visited = new Set();
15
-
16
- function visit(name) {
17
- if (visited.has(name)) {
18
- return;
19
- }
20
- visited.add(name);
21
-
22
- const entry = packageLock.packages[`node_modules/${name}`];
23
- for (const dependency of Object.keys(entry?.dependencies || {})) {
24
- visit(dependency);
25
- }
26
- }
27
-
28
- visit(packageName);
29
- return visited;
30
- }
31
-
32
- const runtimeImports = [
33
- {
34
- packageName: 'turndown-plugin-gfm',
35
- sourceFile: 'src/lib.js',
36
- },
37
- ];
38
-
39
- describe('runtime package dependencies', () => {
40
- test('archiver production dependencies do not include deprecated glob', () => {
41
- const packageLock = readJson('package-lock.json');
42
- const dependencies = collectProductionDependencies(packageLock, 'archiver');
43
-
44
- expect([...dependencies]).not.toContain('glob');
45
- });
46
-
47
- test.each(runtimeImports)(
48
- '$packageName is declared as a production dependency',
49
- ({ packageName, sourceFile }) => {
50
- const source = readFileSync(resolve(jsRoot, sourceFile), 'utf8');
51
- const packageJson = readJson('package.json');
52
-
53
- expect(source).toContain(`from '${packageName}'`);
54
- expect(packageJson.dependencies).toHaveProperty(packageName);
55
- expect(packageJson.devDependencies || {}).not.toHaveProperty(packageName);
56
- }
57
- );
58
-
59
- test.each(runtimeImports)(
60
- '$packageName is not marked dev-only in package-lock.json',
61
- ({ packageName }) => {
62
- const packageLock = readJson('package-lock.json');
63
-
64
- expect(packageLock.packages[''].dependencies).toHaveProperty(packageName);
65
- expect(packageLock.packages[''].devDependencies || {}).not.toHaveProperty(
66
- packageName
67
- );
68
- expect(packageLock.packages[`node_modules/${packageName}`]).toBeDefined();
69
- expect(packageLock.packages[`node_modules/${packageName}`].dev).not.toBe(
70
- true
71
- );
72
- }
73
- );
74
- });
@@ -1,56 +0,0 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import { fileURLToPath } from 'url';
4
- import MarkdownIt from 'markdown-it';
5
- import {
6
- convertHtmlToMarkdown,
7
- convertHtmlToMarkdownEnhanced,
8
- } from '../../src/lib.js';
9
-
10
- const __filename = fileURLToPath(import.meta.url);
11
- const __dirname = path.dirname(__filename);
12
- const HTML = fs.readFileSync(
13
- path.join(__dirname, '..', 'fixtures', 'paragraph-vs-line-break.html'),
14
- 'utf8'
15
- );
16
-
17
- describe('<br><br> coalesces to a paragraph break (not two hard breaks)', () => {
18
- it('renders a paragraph break between Caption A and Caption B (convertHtmlToMarkdown)', () => {
19
- const md = convertHtmlToMarkdown(HTML);
20
- const lines = md.split('\n');
21
- const idxA = lines.findIndex((l) => l.includes('Caption A:'));
22
- const idxB = lines.findIndex((l) => l.includes('Caption B:'));
23
- expect(idxA).toBeGreaterThanOrEqual(0);
24
- expect(idxB).toBeGreaterThan(idxA);
25
-
26
- const between = lines.slice(idxA + 1, idxB);
27
- expect(between.some((l) => l === '')).toBe(true);
28
-
29
- expect(md).not.toMatch(/^[ \t]+ {2}$/m);
30
- });
31
-
32
- it('renders a paragraph break between Caption A and Caption B (convertHtmlToMarkdownEnhanced)', () => {
33
- const { markdown } = convertHtmlToMarkdownEnhanced(HTML);
34
- const lines = markdown.split('\n');
35
- const idxA = lines.findIndex((l) => l.includes('Caption A:'));
36
- const idxB = lines.findIndex((l) => l.includes('Caption B:'));
37
- expect(idxA).toBeGreaterThanOrEqual(0);
38
- expect(idxB).toBeGreaterThan(idxA);
39
-
40
- const between = lines.slice(idxA + 1, idxB);
41
- expect(between.some((l) => l === '')).toBe(true);
42
-
43
- expect(markdown).not.toMatch(/^[ \t]+ {2}$/m);
44
- });
45
-
46
- it('CommonMark renders Caption A and Caption B as separate paragraphs', () => {
47
- const md = convertHtmlToMarkdown(HTML);
48
- const html = new MarkdownIt('commonmark').render(md);
49
- const aIdx = html.indexOf('Caption A:');
50
- const bIdx = html.indexOf('Caption B:');
51
- expect(aIdx).toBeGreaterThan(-1);
52
- expect(bIdx).toBeGreaterThan(aIdx);
53
- const between = html.slice(aIdx, bIdx);
54
- expect(between).toMatch(/<\/p>\s*<p>/);
55
- });
56
- });
@@ -1,110 +0,0 @@
1
- import {
2
- postProcessMarkdown,
3
- applyUnicodeNormalization,
4
- applyLatexSpacingFixes,
5
- applyPercentSignFix,
6
- applyBoldFormattingFixes,
7
- } from '../../src/postprocess.js';
8
-
9
- describe('postprocess module', () => {
10
- describe('applyUnicodeNormalization', () => {
11
- it('preserves non-breaking spaces as &nbsp; entities', () => {
12
- expect(applyUnicodeNormalization('hello\u00A0world')).toBe(
13
- 'hello&nbsp;world'
14
- );
15
- });
16
-
17
- it('normalizes curly quotes to straight quotes', () => {
18
- expect(applyUnicodeNormalization('\u2018hello\u2019')).toBe("'hello'");
19
- expect(applyUnicodeNormalization('\u201Chello\u201D')).toBe('"hello"');
20
- });
21
-
22
- it('normalizes en-dash to hyphen', () => {
23
- expect(applyUnicodeNormalization('2\u20133')).toBe('2-3');
24
- });
25
-
26
- it('normalizes ellipsis', () => {
27
- expect(applyUnicodeNormalization('wait\u2026')).toBe('wait...');
28
- });
29
- });
30
-
31
- describe('applyLatexSpacingFixes', () => {
32
- it('adds space before formula after word character', () => {
33
- const result = applyLatexSpacingFixes('where$x$is');
34
- expect(result).toBe('where $x$ is');
35
- });
36
-
37
- it('trims whitespace inside formula delimiters', () => {
38
- const result = applyLatexSpacingFixes('$ x + y $');
39
- expect(result).toBe('$x + y$');
40
- });
41
-
42
- it('does not modify block formulas', () => {
43
- const result = applyLatexSpacingFixes('$$x + y$$');
44
- expect(result).toBe('$$x + y$$');
45
- });
46
-
47
- it('does not modify blockquote block formulas', () => {
48
- const result = applyLatexSpacingFixes('> $$x + y$$');
49
- expect(result).toBe('> $$x + y$$');
50
- });
51
-
52
- it('handles multiple formulas on one line', () => {
53
- const result = applyLatexSpacingFixes('$a$and$b$');
54
- expect(result).toBe('$a$ and $b$');
55
- });
56
- });
57
-
58
- describe('applyPercentSignFix', () => {
59
- it('fixes percent sign in inline formulas', () => {
60
- const result = applyPercentSignFix('$50\\%$');
61
- expect(result).toBe('$50\\\\%$');
62
- });
63
-
64
- it('fixes \\text{%} notation', () => {
65
- const result = applyPercentSignFix('$50\\text{%}$');
66
- expect(result).toBe('$50\\\\%$');
67
- });
68
- });
69
-
70
- describe('applyBoldFormattingFixes', () => {
71
- it('removes empty bold markers', () => {
72
- const result = applyBoldFormattingFixes('hello ** ** world');
73
- expect(result).toBe('hello world');
74
- });
75
-
76
- it('trims content inside bold markers', () => {
77
- const result = applyBoldFormattingFixes('** hello **');
78
- expect(result).toBe('**hello**');
79
- });
80
-
81
- it('adds space between word and bold', () => {
82
- const result = applyBoldFormattingFixes('word**bold**next');
83
- expect(result).toBe('word **bold** next');
84
- });
85
- });
86
-
87
- describe('postProcessMarkdown', () => {
88
- it('applies all transformations by default', () => {
89
- const input = 'hello\u00A0$x$world';
90
- const result = postProcessMarkdown(input);
91
- expect(result).toContain('hello');
92
- expect(result).toContain('$x$');
93
- expect(result).not.toContain('\u00A0');
94
- });
95
-
96
- it('respects option flags', () => {
97
- const input = 'hello\u00A0world';
98
- const result = postProcessMarkdown(input, {
99
- normalizeUnicode: false,
100
- });
101
- expect(result).toContain('\u00A0');
102
- });
103
-
104
- it('removes stray standalone $ signs', () => {
105
- const input = 'line1\n$\nline2';
106
- const result = postProcessMarkdown(input);
107
- expect(result).toBe('line1\n\nline2');
108
- });
109
- });
110
- });
@@ -1,100 +0,0 @@
1
- import {
2
- copyProxyResponseHeaders,
3
- getProxyHeaderEntries,
4
- } from '../../src/proxy-headers.js';
5
-
6
- describe('proxy header helpers', () => {
7
- it('removes hop-by-hop headers and headers named by Connection', () => {
8
- const entries = getProxyHeaderEntries(
9
- headers({
10
- 'Content-Type': 'text/html',
11
- Connection: 'keep-alive, X-Internal-Hop',
12
- 'Keep-Alive': 'timeout=5',
13
- TE: 'trailers',
14
- Trailer: 'Expires',
15
- Upgrade: 'websocket',
16
- 'Proxy-Authenticate': 'Basic',
17
- 'Proxy-Authorization': 'Basic token',
18
- 'Transfer-Encoding': 'chunked',
19
- 'X-Internal-Hop': 'remove me',
20
- 'X-End-To-End': 'keep me',
21
- })
22
- );
23
-
24
- expect(Object.fromEntries(entries)).toEqual({
25
- 'content-type': 'text/html',
26
- 'x-end-to-end': 'keep me',
27
- });
28
- });
29
-
30
- it('preserves content length only for unencoded streaming responses', () => {
31
- expect(
32
- Object.fromEntries(
33
- getProxyHeaderEntries(
34
- headers({
35
- 'Content-Type': 'text/plain',
36
- 'Content-Length': '12',
37
- }),
38
- { preserveContentLength: true }
39
- )
40
- )
41
- ).toEqual({
42
- 'content-type': 'text/plain',
43
- 'content-length': '12',
44
- });
45
-
46
- expect(
47
- Object.fromEntries(
48
- getProxyHeaderEntries(
49
- headers({
50
- 'Content-Type': 'text/plain',
51
- 'Content-Length': '12',
52
- 'Content-Encoding': 'gzip',
53
- }),
54
- { preserveContentLength: true }
55
- )
56
- )
57
- ).toEqual({
58
- 'content-type': 'text/plain',
59
- });
60
- });
61
-
62
- it('sets a default content type while copying safe headers', () => {
63
- const res = responseRecorder();
64
-
65
- copyProxyResponseHeaders(
66
- headers({
67
- ETag: '"abc"',
68
- }),
69
- res
70
- );
71
-
72
- expect(res.headers).toEqual({
73
- 'content-type': 'text/plain',
74
- etag: '"abc"',
75
- });
76
- });
77
- });
78
-
79
- function headers(values) {
80
- const map = new Map(
81
- Object.entries(values).map(([key, value]) => [key.toLowerCase(), value])
82
- );
83
- return {
84
- get(key) {
85
- return map.get(key.toLowerCase()) || null;
86
- },
87
- entries() {
88
- return map.entries();
89
- },
90
- };
91
- }
92
-
93
- function responseRecorder() {
94
- return {
95
- headers: {},
96
- setHeader(key, value) {
97
- this.headers[key.toLowerCase()] = value;
98
- },
99
- };
100
- }
@@ -1,57 +0,0 @@
1
- import { jest } from '@jest/globals';
2
-
3
- const originalFetch = global.fetch;
4
-
5
- beforeAll(() => {
6
- global.fetch = jest.fn().mockResolvedValue({
7
- text: async () => `
8
- ({
9
- use: async (name) => {
10
- if (name === 'command-stream') return { $: () => ({ run: async () => ({ code: 0, stdout: '', stderr: '' }) }) };
11
- if (name === 'lino-arguments') return { makeConfig: () => ({ shouldPull: false, jsRoot: 'js' }) };
12
- throw new Error('Unexpected dynamic import: ' + name);
13
- }
14
- })
15
- `,
16
- });
17
- });
18
-
19
- afterAll(() => {
20
- global.fetch = originalFetch;
21
- });
22
-
23
- describe('publish verification', () => {
24
- test('keeps polling verification through transient npm 404s', async () => {
25
- const { verifyPublishedVersionWithRunner } =
26
- await import('../../../scripts/publish-to-npm.mjs');
27
-
28
- const runVerify = jest
29
- .fn()
30
- .mockResolvedValueOnce({
31
- code: 1,
32
- stdout: '',
33
- stderr: 'npm error code E404',
34
- })
35
- .mockResolvedValueOnce({
36
- code: 1,
37
- stdout: '',
38
- stderr: 'npm error code E404',
39
- })
40
- .mockResolvedValueOnce({
41
- code: 0,
42
- stdout: '1.7.1\n',
43
- stderr: '',
44
- });
45
- const sleepFn = jest.fn().mockResolvedValue(undefined);
46
-
47
- const published = await verifyPublishedVersionWithRunner(
48
- '1.7.1',
49
- runVerify,
50
- sleepFn
51
- );
52
-
53
- expect(published).toBe(true);
54
- expect(runVerify).toHaveBeenCalledTimes(3);
55
- expect(sleepFn).toHaveBeenCalledTimes(2);
56
- });
57
- });
@@ -1,111 +0,0 @@
1
- import { jest } from '@jest/globals';
2
- import { retry } from '../../src/retry.js';
3
-
4
- jest.setTimeout(30000);
5
-
6
- describe('retry', () => {
7
- it('returns result on first success', async () => {
8
- const fn = jest.fn().mockResolvedValue('ok');
9
- const result = await retry(fn, { retries: 3 });
10
- expect(result).toBe('ok');
11
- expect(fn).toHaveBeenCalledTimes(1);
12
- });
13
-
14
- it('retries on failure and succeeds eventually', async () => {
15
- let calls = 0;
16
- const fn = jest.fn().mockImplementation(async () => {
17
- calls++;
18
- if (calls < 3) {
19
- throw new Error('fail');
20
- }
21
- return 'success';
22
- });
23
-
24
- const result = await retry(fn, { retries: 3, baseDelay: 50 });
25
- expect(result).toBe('success');
26
- expect(fn).toHaveBeenCalledTimes(3);
27
- });
28
-
29
- it('throws after exhausting retries', async () => {
30
- const fn = jest.fn().mockRejectedValue(new Error('persistent failure'));
31
-
32
- await expect(retry(fn, { retries: 2, baseDelay: 50 })).rejects.toThrow(
33
- 'persistent failure'
34
- );
35
- expect(fn).toHaveBeenCalledTimes(3); // initial + 2 retries
36
- });
37
-
38
- it('calls onRetry callback before each retry', async () => {
39
- let calls = 0;
40
- const fn = jest.fn().mockImplementation(async () => {
41
- calls++;
42
- if (calls < 2) {
43
- throw new Error('fail');
44
- }
45
- return 'ok';
46
- });
47
- const onRetry = jest.fn();
48
-
49
- await retry(fn, { retries: 3, baseDelay: 50, onRetry });
50
- expect(onRetry).toHaveBeenCalledTimes(1);
51
- expect(onRetry).toHaveBeenCalledWith(expect.any(Error), 1, 50);
52
- });
53
-
54
- it('uses exponential backoff delays', async () => {
55
- const delays = [];
56
- const onRetry = jest.fn((err, attempt, delay) => {
57
- delays.push(delay);
58
- });
59
-
60
- const fn = jest.fn().mockRejectedValue(new Error('fail'));
61
-
62
- await expect(
63
- retry(fn, {
64
- retries: 3,
65
- baseDelay: 100,
66
- factor: 2,
67
- maxDelay: 1000,
68
- onRetry,
69
- })
70
- ).rejects.toThrow();
71
-
72
- // Delays should be: 100, 200, 400
73
- expect(delays).toEqual([100, 200, 400]);
74
- });
75
-
76
- it('caps delay at maxDelay', async () => {
77
- const delays = [];
78
- const onRetry = jest.fn((err, attempt, delay) => {
79
- delays.push(delay);
80
- });
81
-
82
- const fn = jest.fn().mockRejectedValue(new Error('fail'));
83
-
84
- await expect(
85
- retry(fn, {
86
- retries: 3,
87
- baseDelay: 100,
88
- factor: 10,
89
- maxDelay: 500,
90
- onRetry,
91
- })
92
- ).rejects.toThrow();
93
-
94
- // Delays: min(100, 500)=100, min(1000, 500)=500, min(10000, 500)=500
95
- expect(delays).toEqual([100, 500, 500]);
96
- });
97
-
98
- it('passes attempt number to fn', async () => {
99
- const attempts = [];
100
- const fn = jest.fn().mockImplementation(async (attempt) => {
101
- attempts.push(attempt);
102
- if (attempt < 2) {
103
- throw new Error('fail');
104
- }
105
- return 'ok';
106
- });
107
-
108
- await retry(fn, { retries: 3, baseDelay: 50 });
109
- expect(attempts).toEqual([0, 1, 2]);
110
- });
111
- });