@rulemetric/skills-registry 0.15.1 → 0.17.0
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/dist/github-fetcher.d.ts +24 -1
- package/dist/github-fetcher.js +73 -22
- package/dist/github-fetcher.test.d.ts +2 -0
- package/dist/github-fetcher.test.js +119 -0
- package/package.json +1 -1
package/dist/github-fetcher.d.ts
CHANGED
|
@@ -48,12 +48,27 @@ export interface DraftEntry extends SkillEntry {
|
|
|
48
48
|
* expected to keep its existing entries for that source. Pass `undefined` to
|
|
49
49
|
* always fetch.
|
|
50
50
|
*/
|
|
51
|
-
export declare function fetchSource(source: RegistrySource, previousTreeSha?: string): Promise<FetchSourceResult>;
|
|
51
|
+
export declare function fetchSource(source: RegistrySource, previousTreeSha?: string, signal?: AbortSignal): Promise<FetchSourceResult>;
|
|
52
52
|
export interface FetchAllOptions {
|
|
53
53
|
/** Map of repo → previous tree SHA. Sources with matching SHA are skipped. */
|
|
54
54
|
previousShas?: Map<string, string>;
|
|
55
55
|
/** Called after each source completes (successful or not). */
|
|
56
56
|
onProgress?: (source: string, count: number, skipped: boolean) => void;
|
|
57
|
+
/**
|
|
58
|
+
* Stops the crawl. Sources not yet started are not started; a source in
|
|
59
|
+
* flight stops at its next network hop. Entries already drafted are kept,
|
|
60
|
+
* so the caller decides what a partial registry is worth (`aborted` says
|
|
61
|
+
* it is one). Without this a caller had no way to end a crawl short of
|
|
62
|
+
* killing the process — which is how one kept running for ~16 hours after
|
|
63
|
+
* its job lease was gone (2026-09-10).
|
|
64
|
+
*/
|
|
65
|
+
signal?: AbortSignal;
|
|
66
|
+
/**
|
|
67
|
+
* Test seam: replaces the per-source fetch so the scheduling contract
|
|
68
|
+
* (concurrency, abort, accounting) can be exercised without GitHub.
|
|
69
|
+
* Production callers never set it.
|
|
70
|
+
*/
|
|
71
|
+
fetchSourceImpl?: typeof fetchSource;
|
|
57
72
|
}
|
|
58
73
|
export interface FetchAllResult {
|
|
59
74
|
entries: SkillEntry[];
|
|
@@ -66,6 +81,14 @@ export interface FetchAllResult {
|
|
|
66
81
|
path: string;
|
|
67
82
|
reason: string;
|
|
68
83
|
}>;
|
|
84
|
+
/** True when `options.signal` fired before every source had been visited.
|
|
85
|
+
* `entries` is then a PARTIAL registry and must not replace a full one. */
|
|
86
|
+
aborted: boolean;
|
|
87
|
+
/** Sources whose fetch ran to a result (including skipped-unchanged and
|
|
88
|
+
* failed-and-logged); `sourcesUnfetched` is the remainder the abort
|
|
89
|
+
* prevented. They sum to `sources.length`. */
|
|
90
|
+
sourcesFetched: number;
|
|
91
|
+
sourcesUnfetched: number;
|
|
69
92
|
}
|
|
70
93
|
/**
|
|
71
94
|
* Assign a final, unique id to every drafted entry.
|
package/dist/github-fetcher.js
CHANGED
|
@@ -92,7 +92,7 @@ const rawEtagCache = new Map();
|
|
|
92
92
|
* GETs and basic retry. Raw URLs don't count against the GitHub API quota, so
|
|
93
93
|
* we don't go through octokit here — but we still want timeouts and retries.
|
|
94
94
|
*/
|
|
95
|
-
async function fetchRaw(url, timeoutMs = 10_000, retries = 2) {
|
|
95
|
+
async function fetchRaw(url, timeoutMs = 10_000, retries = 2, signal) {
|
|
96
96
|
const cached = rawEtagCache.get(url);
|
|
97
97
|
const headers = { 'User-Agent': USER_AGENT };
|
|
98
98
|
if (process.env.GITHUB_TOKEN)
|
|
@@ -101,8 +101,12 @@ async function fetchRaw(url, timeoutMs = 10_000, retries = 2) {
|
|
|
101
101
|
headers['If-None-Match'] = cached.etag;
|
|
102
102
|
let lastErr;
|
|
103
103
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
104
|
+
// A caller-side abort (lease lost, budget spent) ends the retry loop; the
|
|
105
|
+
// per-attempt timeout alone would let a dead crawl keep re-trying.
|
|
106
|
+
if (signal?.aborted)
|
|
107
|
+
return { status: 0, body: null, fromCache: false };
|
|
104
108
|
try {
|
|
105
|
-
const resp = await fetch(url, { headers, signal:
|
|
109
|
+
const resp = await fetch(url, { headers, signal: withTimeout(timeoutMs, signal) });
|
|
106
110
|
if (resp.status === 304 && cached) {
|
|
107
111
|
return { status: 304, body: cached.body, fromCache: true };
|
|
108
112
|
}
|
|
@@ -126,6 +130,8 @@ async function fetchRaw(url, timeoutMs = 10_000, retries = 2) {
|
|
|
126
130
|
}
|
|
127
131
|
catch (err) {
|
|
128
132
|
lastErr = err;
|
|
133
|
+
if (signal?.aborted)
|
|
134
|
+
return { status: 0, body: null, fromCache: false };
|
|
129
135
|
await sleep(250 * 2 ** attempt);
|
|
130
136
|
}
|
|
131
137
|
}
|
|
@@ -133,18 +139,35 @@ async function fetchRaw(url, timeoutMs = 10_000, retries = 2) {
|
|
|
133
139
|
return { status: 0, body: null, fromCache: false };
|
|
134
140
|
}
|
|
135
141
|
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
|
142
|
+
/**
|
|
143
|
+
* Per-request timeout combined with the caller's abort signal, so a crawl the
|
|
144
|
+
* caller has stopped does not wait out its own timeout first. `AbortSignal.any`
|
|
145
|
+
* is Node 20.3+; the package still declares >=18.18, so fall back to the
|
|
146
|
+
* timeout alone (the caller-side `aborted` checks between hops still bound
|
|
147
|
+
* the work — just one request later).
|
|
148
|
+
*/
|
|
149
|
+
function withTimeout(timeoutMs, signal) {
|
|
150
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
151
|
+
if (!signal)
|
|
152
|
+
return timeout;
|
|
153
|
+
const any = AbortSignal.any;
|
|
154
|
+
return typeof any === 'function' ? any([timeout, signal]) : timeout;
|
|
155
|
+
}
|
|
136
156
|
/**
|
|
137
157
|
* Fetch repo metadata + tree SHA in one round trip via the API. Lets callers
|
|
138
158
|
* compare treeSha against a previous run to skip unchanged repos entirely.
|
|
139
159
|
*/
|
|
140
|
-
async function fetchRepoMeta(repo) {
|
|
160
|
+
async function fetchRepoMeta(repo, signal) {
|
|
141
161
|
const [owner, name] = repo.split('/');
|
|
142
162
|
if (!owner || !name)
|
|
143
163
|
return null;
|
|
144
164
|
try {
|
|
145
|
-
const
|
|
165
|
+
const request = signal ? { signal } : undefined;
|
|
166
|
+
const { data } = await getOctokit().rest.repos.get({ owner, repo: name, request });
|
|
167
|
+
if (signal?.aborted)
|
|
168
|
+
return null;
|
|
146
169
|
const branchData = await getOctokit().rest.repos.getBranch({
|
|
147
|
-
owner, repo: name, branch: data.default_branch,
|
|
170
|
+
owner, repo: name, branch: data.default_branch, request,
|
|
148
171
|
});
|
|
149
172
|
return {
|
|
150
173
|
defaultBranch: data.default_branch,
|
|
@@ -155,7 +178,7 @@ async function fetchRepoMeta(repo) {
|
|
|
155
178
|
}
|
|
156
179
|
catch (err) {
|
|
157
180
|
const status = err.status;
|
|
158
|
-
if (status !== 404)
|
|
181
|
+
if (status !== 404 && !signal?.aborted)
|
|
159
182
|
console.warn(`fetchRepoMeta failed for ${repo}:`, err);
|
|
160
183
|
return null;
|
|
161
184
|
}
|
|
@@ -171,30 +194,30 @@ async function fetchRepoMeta(repo) {
|
|
|
171
194
|
* Returns null on any failure (404, rate-limit give-up, network) so the caller
|
|
172
195
|
* can fall back to the repo pushed_at without aborting the whole build.
|
|
173
196
|
*/
|
|
174
|
-
async function fetchFileCommitDate(repo, filePath) {
|
|
197
|
+
async function fetchFileCommitDate(repo, filePath, signal) {
|
|
175
198
|
const [owner, name] = repo.split('/');
|
|
176
|
-
if (!owner || !name)
|
|
199
|
+
if (!owner || !name || signal?.aborted)
|
|
177
200
|
return null;
|
|
178
201
|
try {
|
|
179
202
|
const { data } = await getOctokit().rest.repos.listCommits({
|
|
180
|
-
owner, repo: name, path: filePath, per_page: 1,
|
|
203
|
+
owner, repo: name, path: filePath, per_page: 1, request: signal ? { signal } : undefined,
|
|
181
204
|
});
|
|
182
205
|
return data[0]?.commit?.committer?.date ?? data[0]?.commit?.author?.date ?? null;
|
|
183
206
|
}
|
|
184
207
|
catch (err) {
|
|
185
208
|
const status = err.status;
|
|
186
|
-
if (status !== 404)
|
|
209
|
+
if (status !== 404 && !signal?.aborted)
|
|
187
210
|
console.warn(`fetchFileCommitDate failed for ${repo}/${filePath}:`, err);
|
|
188
211
|
return null;
|
|
189
212
|
}
|
|
190
213
|
}
|
|
191
|
-
async function fetchTree(repo, treeSha) {
|
|
214
|
+
async function fetchTree(repo, treeSha, signal) {
|
|
192
215
|
const [owner, name] = repo.split('/');
|
|
193
|
-
if (!owner || !name)
|
|
216
|
+
if (!owner || !name || signal?.aborted)
|
|
194
217
|
return [];
|
|
195
218
|
try {
|
|
196
219
|
const { data } = await getOctokit().rest.git.getTree({
|
|
197
|
-
owner, repo: name, tree_sha: treeSha, recursive: 'true',
|
|
220
|
+
owner, repo: name, tree_sha: treeSha, recursive: 'true', request: signal ? { signal } : undefined,
|
|
198
221
|
});
|
|
199
222
|
if (data.truncated)
|
|
200
223
|
console.warn(`Tree truncated for ${repo} — large repo, some files skipped`);
|
|
@@ -207,7 +230,8 @@ async function fetchTree(repo, treeSha) {
|
|
|
207
230
|
return out;
|
|
208
231
|
}
|
|
209
232
|
catch (err) {
|
|
210
|
-
|
|
233
|
+
if (!signal?.aborted)
|
|
234
|
+
console.warn(`fetchTree failed for ${repo}@${treeSha}:`, err);
|
|
211
235
|
return [];
|
|
212
236
|
}
|
|
213
237
|
}
|
|
@@ -286,15 +310,15 @@ const FILE_CONCURRENCY = pLimit(8);
|
|
|
286
310
|
* expected to keep its existing entries for that source. Pass `undefined` to
|
|
287
311
|
* always fetch.
|
|
288
312
|
*/
|
|
289
|
-
export async function fetchSource(source, previousTreeSha) {
|
|
290
|
-
const meta = await fetchRepoMeta(source.repo);
|
|
313
|
+
export async function fetchSource(source, previousTreeSha, signal) {
|
|
314
|
+
const meta = await fetchRepoMeta(source.repo, signal);
|
|
291
315
|
if (!meta)
|
|
292
316
|
return { entries: [], treeSha: null, skipped: false, dropped: [] };
|
|
293
317
|
if (previousTreeSha && previousTreeSha === meta.treeSha) {
|
|
294
318
|
return { entries: [], treeSha: meta.treeSha, skipped: true, dropped: [] };
|
|
295
319
|
}
|
|
296
320
|
const branch = source.branch ?? meta.defaultBranch;
|
|
297
|
-
const tree = await fetchTree(source.repo, meta.treeSha);
|
|
321
|
+
const tree = await fetchTree(source.repo, meta.treeSha, signal);
|
|
298
322
|
const matchedFiles = tree.filter(item => {
|
|
299
323
|
const ext = path.extname(item.path).toLowerCase();
|
|
300
324
|
if (SKIP_EXTENSIONS.has(ext))
|
|
@@ -305,8 +329,12 @@ export async function fetchSource(source, previousTreeSha) {
|
|
|
305
329
|
});
|
|
306
330
|
const dropped = [];
|
|
307
331
|
const entries = await Promise.all(matchedFiles.map(file => FILE_CONCURRENCY(async () => {
|
|
332
|
+
// Checked per file, not just per source: a large repo is hundreds of
|
|
333
|
+
// raw fetches, and the caller's stop must land inside it.
|
|
334
|
+
if (signal?.aborted)
|
|
335
|
+
return null;
|
|
308
336
|
const url = `${GITHUB_RAW}/${source.repo}/${branch}/${file.path}`;
|
|
309
|
-
const result = await fetchRaw(url);
|
|
337
|
+
const result = await fetchRaw(url, undefined, undefined, signal);
|
|
310
338
|
if (!result.body || result.body.trim().length < 20)
|
|
311
339
|
return null;
|
|
312
340
|
const content = result.body;
|
|
@@ -327,7 +355,7 @@ export async function fetchSource(source, previousTreeSha) {
|
|
|
327
355
|
// Real "last updated" date: the file's most recent commit. Falls back
|
|
328
356
|
// to the repo's pushed_at if the per-file lookup fails (rate limit /
|
|
329
357
|
// error), so the field is populated even on a degraded run.
|
|
330
|
-
const sourceUpdatedAt = (await fetchFileCommitDate(source.repo, file.path)) ?? meta.pushedAt ?? undefined;
|
|
358
|
+
const sourceUpdatedAt = (await fetchFileCommitDate(source.repo, file.path, signal)) ?? meta.pushedAt ?? undefined;
|
|
331
359
|
return {
|
|
332
360
|
// Provisional. Final ids are assigned across all sources at once so
|
|
333
361
|
// two repos racing for the same id cannot decide it between them.
|
|
@@ -420,10 +448,20 @@ export async function fetchAllSources(sources, options = {}) {
|
|
|
420
448
|
const drafts = [];
|
|
421
449
|
const treeShas = new Map();
|
|
422
450
|
const dropped = [];
|
|
451
|
+
const fetchOne = options.fetchSourceImpl ?? fetchSource;
|
|
452
|
+
const signal = options.signal;
|
|
453
|
+
let sourcesFetched = 0;
|
|
454
|
+
let sourcesUnfetched = 0;
|
|
423
455
|
await Promise.all(sources.map(source => SOURCE_CONCURRENCY(async () => {
|
|
456
|
+
// The limiter releases queued sources one at a time after the abort;
|
|
457
|
+
// each must decline rather than start a fresh crawl.
|
|
458
|
+
if (signal?.aborted) {
|
|
459
|
+
sourcesUnfetched++;
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
424
462
|
try {
|
|
425
463
|
const previous = options.previousShas?.get(source.repo);
|
|
426
|
-
const result = await
|
|
464
|
+
const result = await fetchOne(source, previous, signal);
|
|
427
465
|
if (result.treeSha)
|
|
428
466
|
treeShas.set(source.repo, result.treeSha);
|
|
429
467
|
drafts.push(...result.entries);
|
|
@@ -431,11 +469,24 @@ export async function fetchAllSources(sources, options = {}) {
|
|
|
431
469
|
options.onProgress?.(source.repo, result.entries.length, result.skipped);
|
|
432
470
|
}
|
|
433
471
|
catch (err) {
|
|
434
|
-
|
|
472
|
+
if (!signal?.aborted)
|
|
473
|
+
console.error(`Failed to fetch ${source.repo}:`, err);
|
|
435
474
|
options.onProgress?.(source.repo, 0, false);
|
|
436
475
|
}
|
|
476
|
+
// A source cut off mid-flight is counted as fetched: it ran, and its
|
|
477
|
+
// partial entries are in `drafts`. `aborted` is what tells the caller
|
|
478
|
+
// the whole result is partial.
|
|
479
|
+
sourcesFetched++;
|
|
437
480
|
})));
|
|
438
481
|
const { entries, collisions } = assignIds(drafts);
|
|
439
|
-
return {
|
|
482
|
+
return {
|
|
483
|
+
entries,
|
|
484
|
+
treeShas,
|
|
485
|
+
collisions,
|
|
486
|
+
dropped,
|
|
487
|
+
aborted: Boolean(signal?.aborted),
|
|
488
|
+
sourcesFetched,
|
|
489
|
+
sourcesUnfetched,
|
|
490
|
+
};
|
|
440
491
|
}
|
|
441
492
|
//# sourceMappingURL=github-fetcher.js.map
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { fetchAllSources } from './github-fetcher.js';
|
|
3
|
+
/**
|
|
4
|
+
* The scheduling contract of fetchAllSources, exercised through the
|
|
5
|
+
* `fetchSourceImpl` seam so no GitHub call is made.
|
|
6
|
+
*
|
|
7
|
+
* Origin 2026-09-10: a caller had no way to end a crawl short of killing the
|
|
8
|
+
* process, and one ran ~16 hours past the point its job lease was gone. The
|
|
9
|
+
* contract: `signal` stops sources from STARTING, entries already drafted are
|
|
10
|
+
* kept, and the result says it is partial.
|
|
11
|
+
*/
|
|
12
|
+
function source(repo) {
|
|
13
|
+
return { repo, patterns: ['**/*.md'], defaultTool: 'claude_code', defaultCategory: 'other' };
|
|
14
|
+
}
|
|
15
|
+
function draft(repo, n) {
|
|
16
|
+
const id = `${repo.replace('/', '--')}--${n}`;
|
|
17
|
+
return {
|
|
18
|
+
id,
|
|
19
|
+
_idCandidates: [id],
|
|
20
|
+
_rank: 0,
|
|
21
|
+
kind: 'skill',
|
|
22
|
+
name: `Skill ${n}`,
|
|
23
|
+
description: 'd',
|
|
24
|
+
category: 'other',
|
|
25
|
+
tags: [],
|
|
26
|
+
tools: ['claude_code'],
|
|
27
|
+
source: { repo, path: `${n}.md`, url: 'u' },
|
|
28
|
+
content: `# ${n}`,
|
|
29
|
+
popularity: { stars: 0 },
|
|
30
|
+
updatedAt: '2026-09-10T00:00:00.000Z',
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function fetched(repo, count) {
|
|
34
|
+
return {
|
|
35
|
+
entries: Array.from({ length: count }, (_, i) => draft(repo, i)),
|
|
36
|
+
treeSha: `sha-${repo}`,
|
|
37
|
+
skipped: false,
|
|
38
|
+
dropped: [],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function deferred() {
|
|
42
|
+
let resolve;
|
|
43
|
+
const promise = new Promise((r) => { resolve = r; });
|
|
44
|
+
return { promise, resolve };
|
|
45
|
+
}
|
|
46
|
+
describe('fetchAllSources abort contract', () => {
|
|
47
|
+
it('without a signal visits every source and reports a complete result', async () => {
|
|
48
|
+
const sources = ['a/1', 'b/2', 'c/3'].map(source);
|
|
49
|
+
const impl = vi.fn(async (s) => fetched(s.repo, 2));
|
|
50
|
+
const result = await fetchAllSources(sources, { fetchSourceImpl: impl });
|
|
51
|
+
expect(impl).toHaveBeenCalledTimes(3);
|
|
52
|
+
expect(result.entries).toHaveLength(6);
|
|
53
|
+
expect(result).toMatchObject({ aborted: false, sourcesFetched: 3, sourcesUnfetched: 0 });
|
|
54
|
+
});
|
|
55
|
+
it('with an already-aborted signal starts no source at all', async () => {
|
|
56
|
+
const sources = ['a/1', 'b/2', 'c/3'].map(source);
|
|
57
|
+
const impl = vi.fn(async (s) => fetched(s.repo, 2));
|
|
58
|
+
const ctrl = new AbortController();
|
|
59
|
+
ctrl.abort();
|
|
60
|
+
const result = await fetchAllSources(sources, { fetchSourceImpl: impl, signal: ctrl.signal });
|
|
61
|
+
expect(impl).not.toHaveBeenCalled();
|
|
62
|
+
expect(result.entries).toEqual([]);
|
|
63
|
+
expect(result).toMatchObject({ aborted: true, sourcesFetched: 0, sourcesUnfetched: 3 });
|
|
64
|
+
});
|
|
65
|
+
it('aborting mid-crawl keeps what was drafted, starts nothing further, and marks the result partial', async () => {
|
|
66
|
+
// Six sources against the fetcher's concurrency of four: four start, two
|
|
67
|
+
// wait in the limiter's queue. The abort lands while all four are in
|
|
68
|
+
// flight; the two queued ones must decline when their turn comes.
|
|
69
|
+
const sources = ['a/1', 'b/2', 'c/3', 'd/4', 'e/5', 'f/6'].map(source);
|
|
70
|
+
const gates = new Map();
|
|
71
|
+
const impl = vi.fn((s, _prev, signal) => {
|
|
72
|
+
expect(signal).toBeInstanceOf(AbortSignal);
|
|
73
|
+
const gate = deferred();
|
|
74
|
+
gates.set(s.repo, gate);
|
|
75
|
+
return gate.promise;
|
|
76
|
+
});
|
|
77
|
+
const ctrl = new AbortController();
|
|
78
|
+
const progress = [];
|
|
79
|
+
const run = fetchAllSources(sources, {
|
|
80
|
+
fetchSourceImpl: impl,
|
|
81
|
+
signal: ctrl.signal,
|
|
82
|
+
onProgress: (repo) => progress.push(repo),
|
|
83
|
+
});
|
|
84
|
+
await vi.waitFor(() => expect(impl).toHaveBeenCalledTimes(4));
|
|
85
|
+
// One source completes normally, then the caller stops the crawl while
|
|
86
|
+
// three are still in flight.
|
|
87
|
+
gates.get('a/1').resolve(fetched('a/1', 3));
|
|
88
|
+
ctrl.abort();
|
|
89
|
+
// The in-flight three return whatever they had when they noticed.
|
|
90
|
+
gates.get('b/2').resolve(fetched('b/2', 1));
|
|
91
|
+
gates.get('c/3').resolve({ entries: [], treeSha: null, skipped: false, dropped: [] });
|
|
92
|
+
gates.get('d/4').resolve({ entries: [], treeSha: null, skipped: false, dropped: [] });
|
|
93
|
+
const result = await run;
|
|
94
|
+
expect(impl).toHaveBeenCalledTimes(4);
|
|
95
|
+
expect(result.entries.map((e) => e.id).sort()).toEqual(['a--1--0', 'a--1--1', 'a--1--2', 'b--2--0'].sort());
|
|
96
|
+
expect(result).toMatchObject({ aborted: true, sourcesFetched: 4, sourcesUnfetched: 2 });
|
|
97
|
+
expect(progress.sort()).toEqual(['a/1', 'b/2', 'c/3', 'd/4']);
|
|
98
|
+
// The two that never ran are not reported as progress either.
|
|
99
|
+
expect(progress).not.toContain('e/5');
|
|
100
|
+
});
|
|
101
|
+
it('a source that throws after the abort is counted as fetched and does not surface as an error', async () => {
|
|
102
|
+
const sources = ['a/1'].map(source);
|
|
103
|
+
const ctrl = new AbortController();
|
|
104
|
+
const impl = vi.fn(async () => {
|
|
105
|
+
ctrl.abort();
|
|
106
|
+
throw new Error('The operation was aborted');
|
|
107
|
+
});
|
|
108
|
+
const errors = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
109
|
+
try {
|
|
110
|
+
const result = await fetchAllSources(sources, { fetchSourceImpl: impl, signal: ctrl.signal });
|
|
111
|
+
expect(result).toMatchObject({ aborted: true, sourcesFetched: 1, sourcesUnfetched: 0, entries: [] });
|
|
112
|
+
expect(errors).not.toHaveBeenCalled();
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
errors.mockRestore();
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
//# sourceMappingURL=github-fetcher.test.js.map
|