@link-assistant/web-capture 2.0.0 → 2.0.2

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 (117) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/bin/web-capture.js +2 -5
  3. package/package.json +36 -23
  4. package/src/archive.js +0 -1
  5. package/src/binary-download.js +0 -1
  6. package/src/docx.js +13 -10
  7. package/src/fetch.js +0 -1
  8. package/src/gdocs-images.js +0 -1
  9. package/src/gdocs.js +0 -1
  10. package/src/github.js +0 -1
  11. package/src/lib.js +0 -1
  12. package/src/markdown.js +0 -1
  13. package/src/shared-dialog.js +0 -1
  14. package/src/stream.js +0 -1
  15. package/src/transport.js +23 -5
  16. package/src/txt.js +0 -1
  17. package/.changeset/README.md +0 -8
  18. package/.changeset/config.json +0 -11
  19. package/.dockerignore +0 -6
  20. package/.jscpd.json +0 -19
  21. package/.lenv +0 -8
  22. package/.prettierignore +0 -8
  23. package/.prettierrc +0 -10
  24. package/Dockerfile +0 -52
  25. package/docker-compose.yml +0 -11
  26. package/eslint.config.js +0 -112
  27. package/examples/js/capture_cache.js +0 -12
  28. package/examples/js/engine_comparison.js +0 -39
  29. package/examples/js/html_download.js +0 -23
  30. package/examples/js/image_download.js +0 -36
  31. package/examples/js/markdown_download.js +0 -23
  32. package/examples/js/playwright_example.js +0 -23
  33. package/examples/python/capture_client.py +0 -20
  34. package/examples/python/html_download.py +0 -3
  35. package/examples/python/image_download.py +0 -16
  36. package/examples/python/markdown_download.py +0 -3
  37. package/examples/python/playwright_example.py +0 -22
  38. package/jest.config.mjs +0 -17
  39. package/tests/ci/dependency-freshness.test.js +0 -74
  40. package/tests/ci/detect-code-changes.test.js +0 -187
  41. package/tests/ci/docker-install.test.js +0 -31
  42. package/tests/ci/rust-dockerfile.test.js +0 -54
  43. package/tests/ci/workflow-policy.test.js +0 -79
  44. package/tests/e2e/docker.test.js +0 -186
  45. package/tests/e2e/process.test.js +0 -215
  46. package/tests/fixtures/bold-coalesce-and-leakage.html +0 -27
  47. package/tests/fixtures/deepwiki/deepwiki-full-page.png +0 -0
  48. package/tests/fixtures/deepwiki/index.md +0 -1500
  49. package/tests/fixtures/deepwiki/part-1.md +0 -776
  50. package/tests/fixtures/habr-articles.js +0 -45
  51. package/tests/fixtures/heading-numbering.html +0 -19
  52. package/tests/fixtures/list-item-with-br.html +0 -8
  53. package/tests/fixtures/ol-continuous-numbering.html +0 -12
  54. package/tests/fixtures/paragraph-vs-line-break.html +0 -11
  55. package/tests/helpers/nock.js +0 -10
  56. package/tests/helpers/screenshot.js +0 -10
  57. package/tests/helpers/search-fixture.cjs +0 -17
  58. package/tests/helpers/temporary-directory.js +0 -12
  59. package/tests/integration/api-endpoints.test.js +0 -418
  60. package/tests/integration/archive-default-layout.test.js +0 -37
  61. package/tests/integration/browser-engines.test.js +0 -80
  62. package/tests/integration/browser-packaged-path.test.js +0 -45
  63. package/tests/integration/deepwiki-download.test.js +0 -144
  64. package/tests/integration/deepwiki-rendered-page.mock.test.js +0 -121
  65. package/tests/integration/formalai-contract.test.js +0 -214
  66. package/tests/integration/gdocs-public-doc.test.js +0 -315
  67. package/tests/integration/github-readme.test.js +0 -133
  68. package/tests/integration/google-drive-image.test.js +0 -246
  69. package/tests/integration/habr-article.test.js +0 -337
  70. package/tests/integration/search-endpoint.test.js +0 -88
  71. package/tests/integration/stackoverflow-download.test.js +0 -173
  72. package/tests/integration/wikipedia-download.test.js +0 -134
  73. package/tests/jest.setup.mjs +0 -7
  74. package/tests/mock/index.test.js +0 -225
  75. package/tests/unit/animation.test.js +0 -33
  76. package/tests/unit/batch.test.js +0 -114
  77. package/tests/unit/bold-coalesce-and-leakage.test.js +0 -54
  78. package/tests/unit/browser-launch-options.test.js +0 -42
  79. package/tests/unit/browser.test.js +0 -172
  80. package/tests/unit/cache.test.js +0 -182
  81. package/tests/unit/cli.test.js +0 -399
  82. package/tests/unit/convertGoogleDriveUrl.test.js +0 -89
  83. package/tests/unit/convertRelativeUrls.test.js +0 -34
  84. package/tests/unit/extract-images.test.js +0 -259
  85. package/tests/unit/fetch-html.test.js +0 -54
  86. package/tests/unit/figures.test.js +0 -74
  87. package/tests/unit/gdocs-export-headers.test.js +0 -28
  88. package/tests/unit/gdocs-preprocess.test.js +0 -36
  89. package/tests/unit/gdocs.test.js +0 -1490
  90. package/tests/unit/github.test.js +0 -156
  91. package/tests/unit/heading-image.test.js +0 -49
  92. package/tests/unit/heading-numbering.test.js +0 -28
  93. package/tests/unit/html2md-br-in-list-item.test.js +0 -23
  94. package/tests/unit/html2md-ol-numbering.test.js +0 -20
  95. package/tests/unit/html2md.test.js +0 -695
  96. package/tests/unit/image-mode-defaults.test.js +0 -54
  97. package/tests/unit/kreuzberg.test.js +0 -249
  98. package/tests/unit/latex.test.js +0 -123
  99. package/tests/unit/localize-images.test.js +0 -114
  100. package/tests/unit/markdown.test.js +0 -36
  101. package/tests/unit/metadata.test.js +0 -140
  102. package/tests/unit/package-runtime-dependencies.test.js +0 -74
  103. package/tests/unit/paragraph-vs-line-break.test.js +0 -56
  104. package/tests/unit/postprocess.test.js +0 -110
  105. package/tests/unit/proxy-headers.test.js +0 -100
  106. package/tests/unit/publish-verification.test.js +0 -54
  107. package/tests/unit/retry.test.js +0 -111
  108. package/tests/unit/search.test.js +0 -339
  109. package/tests/unit/shared-dialog.test.js +0 -173
  110. package/tests/unit/stackoverflow.test.js +0 -32
  111. package/tests/unit/stream.test.js +0 -83
  112. package/tests/unit/verify.test.js +0 -182
  113. package/tests/xpaste/xpaste-screenshot-markdown.mock.test.js +0 -375
  114. package/tests/xpaste/xpaste.e2e.test.js +0 -36
  115. package/tests/xpaste/xpaste.integration.test.js +0 -41
  116. package/tests/xpaste/xpaste.mock.test.js +0 -261
  117. 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,54 +0,0 @@
1
- import { jest } from '@jest/globals';
2
-
3
- jest.unstable_mockModule('execa', () => ({
4
- $: () => async () => ({ exitCode: 0, stdout: '', stderr: '' }),
5
- }));
6
- jest.unstable_mockModule('lino-arguments', () => ({
7
- makeConfig: () => ({ shouldPull: false, jsRoot: 'js' }),
8
- }));
9
-
10
- beforeAll(() => {
11
- jest
12
- .spyOn(global, 'fetch')
13
- .mockRejectedValue(new Error('unexpected remote code download'));
14
- });
15
-
16
- afterAll(() => {
17
- global.fetch.mockRestore();
18
- });
19
-
20
- describe('publish verification', () => {
21
- test('keeps polling verification through transient npm 404s', async () => {
22
- const { verifyPublishedVersionWithRunner } =
23
- await import('../../../scripts/publish-to-npm.mjs');
24
-
25
- const runVerify = jest
26
- .fn()
27
- .mockResolvedValueOnce({
28
- exitCode: 1,
29
- stdout: '',
30
- stderr: 'npm error code E404',
31
- })
32
- .mockResolvedValueOnce({
33
- exitCode: 1,
34
- stdout: '',
35
- stderr: 'npm error code E404',
36
- })
37
- .mockResolvedValueOnce({
38
- exitCode: 0,
39
- stdout: '1.7.1\n',
40
- stderr: '',
41
- });
42
- const sleepFn = jest.fn().mockResolvedValue(undefined);
43
-
44
- const published = await verifyPublishedVersionWithRunner(
45
- '1.7.1',
46
- runVerify,
47
- sleepFn
48
- );
49
-
50
- expect(published).toBe(true);
51
- expect(runVerify).toHaveBeenCalledTimes(3);
52
- expect(sleepFn).toHaveBeenCalledTimes(2);
53
- });
54
- });
@@ -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
- });