mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-09-01 05:08:38 +00:00
Fix search result limiting after ranking (#277)
This commit is contained in:
@@ -27,7 +27,8 @@ Indexer aggregator for searching multiple torrent/usenet indexers simultaneously
|
|||||||
- Minimum score threshold: 50/100
|
- Minimum score threshold: 50/100
|
||||||
- Filters applied after ranking to remove poor matches
|
- Filters applied after ranking to remove poor matches
|
||||||
- Ensures at least basic title/author match quality
|
- Ensures at least basic title/author match quality
|
||||||
- maxResults: 100 (increased from 50 for broader search)
|
- Audiobook searches rank the full deduplicated result set, then retain the best 100
|
||||||
|
- Interactive responses expose `truncated` and the pre-ranking `rawCount`
|
||||||
|
|
||||||
**Example:** "Season of Storms" → finds all "Season of Storms" torrents → ranks by author match → filters score < 50
|
**Example:** "Season of Storms" → finds all "Season of Storms" torrents → ranks by author match → filters score < 50
|
||||||
|
|
||||||
@@ -66,16 +67,17 @@ interface TorrentResult {
|
|||||||
|
|
||||||
**Manual Search** (`POST /api/requests/{id}/manual-search`)
|
**Manual Search** (`POST /api/requests/{id}/manual-search`)
|
||||||
- Triggers automatic search job for requests with status: pending, failed, awaiting_search
|
- Triggers automatic search job for requests with status: pending, failed, awaiting_search
|
||||||
- Searches only enabled indexers (title only, maxResults: 100)
|
- Searches only enabled indexers (title only)
|
||||||
- Ranks all results, filters scores < 50
|
- Ranks all results, filters scores < 50
|
||||||
|
- Retains the best 100 qualifying results after ranking
|
||||||
- Selects best torrent from filtered results
|
- Selects best torrent from filtered results
|
||||||
- Updates request status to 'pending'
|
- Updates request status to 'pending'
|
||||||
|
|
||||||
**Interactive Search** (`POST /api/requests/{id}/interactive-search`)
|
**Interactive Search** (`POST /api/requests/{id}/interactive-search`)
|
||||||
- Returns ranked torrent results for user selection
|
- Returns ranked torrent results for user selection
|
||||||
- Searches only enabled indexers (title only or custom, maxResults: 100)
|
- Searches only enabled indexers (title only or custom)
|
||||||
- Accepts optional custom search title in request body
|
- Accepts optional custom search title in request body
|
||||||
- Ranks all results, filters scores < 50
|
- Ranks all results, then returns the best 100 with truncation metadata
|
||||||
- Shows table with: rank, title, size, quality score, seeders, indexer, publish date
|
- Shows table with: rank, title, size, quality score, seeders, indexer, publish date
|
||||||
- Editable title field allows search refinement
|
- Editable title field allows search refinement
|
||||||
- Available for same statuses as manual search
|
- Available for same statuses as manual search
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { z } from 'zod';
|
|||||||
import { RMABLogger } from '@/lib/utils/logger';
|
import { RMABLogger } from '@/lib/utils/logger';
|
||||||
|
|
||||||
const logger = RMABLogger.create('API.AudiobookSearch');
|
const logger = RMABLogger.create('API.AudiobookSearch');
|
||||||
|
const MAX_RANKED_RESULTS = 100;
|
||||||
|
|
||||||
const SearchSchema = z.object({
|
const SearchSchema = z.object({
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
@@ -98,7 +99,6 @@ export async function POST(request: NextRequest) {
|
|||||||
const groupResults = await prowlarr.searchWithVariations(title, author, {
|
const groupResults = await prowlarr.searchWithVariations(title, author, {
|
||||||
categories: group.categories,
|
categories: group.categories,
|
||||||
indexerIds: group.indexerIds,
|
indexerIds: group.indexerIds,
|
||||||
maxResults: 100, // Limit per group
|
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.debug(`Group ${i + 1} returned ${groupResults.length} results`);
|
logger.debug(`Group ${i + 1} returned ${groupResults.length} results`);
|
||||||
@@ -116,6 +116,8 @@ export async function POST(request: NextRequest) {
|
|||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
results: [],
|
results: [],
|
||||||
|
truncated: false,
|
||||||
|
rawCount: 0,
|
||||||
message: 'No torrents/nzbs found',
|
message: 'No torrents/nzbs found',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -167,6 +169,14 @@ export async function POST(request: NextRequest) {
|
|||||||
// User can see scores and make their own decision
|
// User can see scores and make their own decision
|
||||||
logger.debug(`Ranked ${rankedResults.length} results (no threshold filter - user decides)`);
|
logger.debug(`Ranked ${rankedResults.length} results (no threshold filter - user decides)`);
|
||||||
|
|
||||||
|
const truncated = rankedResults.length > MAX_RANKED_RESULTS;
|
||||||
|
const limitedRankedResults = rankedResults.slice(0, MAX_RANKED_RESULTS);
|
||||||
|
if (truncated) {
|
||||||
|
logger.info(`Audiobook search truncated after ranking: returning top ${MAX_RANKED_RESULTS} of ${rankedResults.length} ranked results`, {
|
||||||
|
rawCount: results.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Log top 3 results with detailed score breakdown for debugging
|
// Log top 3 results with detailed score breakdown for debugging
|
||||||
const top3 = rankedResults.slice(0, 3);
|
const top3 = rankedResults.slice(0, 3);
|
||||||
if (top3.length > 0) {
|
if (top3.length > 0) {
|
||||||
@@ -198,7 +208,7 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add rank position to each result
|
// Add rank position to each result
|
||||||
const resultsWithRank = rankedResults.map((result, index) => ({
|
const resultsWithRank = limitedRankedResults.map((result, index) => ({
|
||||||
...result,
|
...result,
|
||||||
rank: index + 1,
|
rank: index + 1,
|
||||||
}));
|
}));
|
||||||
@@ -206,8 +216,10 @@ export async function POST(request: NextRequest) {
|
|||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
results: resultsWithRank,
|
results: resultsWithRank,
|
||||||
message: rankedResults.length > 0
|
truncated,
|
||||||
? `Found ${rankedResults.length} results`
|
rawCount: results.length,
|
||||||
|
message: limitedRankedResults.length > 0
|
||||||
|
? `Found ${limitedRankedResults.length} results`
|
||||||
: 'No results found',
|
: 'No results found',
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { RMABLogger } from '@/lib/utils/logger';
|
|||||||
import { resolveInteractiveSearchAccess } from '@/lib/utils/permissions';
|
import { resolveInteractiveSearchAccess } from '@/lib/utils/permissions';
|
||||||
|
|
||||||
const logger = RMABLogger.create('API.InteractiveSearch');
|
const logger = RMABLogger.create('API.InteractiveSearch');
|
||||||
|
const MAX_RANKED_RESULTS = 100;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /api/requests/[id]/interactive-search
|
* POST /api/requests/[id]/interactive-search
|
||||||
@@ -151,7 +152,6 @@ export async function POST(
|
|||||||
const groupResults = await prowlarr.searchWithVariations(searchTitle, searchAuthor, {
|
const groupResults = await prowlarr.searchWithVariations(searchTitle, searchAuthor, {
|
||||||
categories: group.categories,
|
categories: group.categories,
|
||||||
indexerIds: group.indexerIds,
|
indexerIds: group.indexerIds,
|
||||||
maxResults: 100,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.debug(`Group ${i + 1} returned ${groupResults.length} results`);
|
logger.debug(`Group ${i + 1} returned ${groupResults.length} results`);
|
||||||
@@ -169,6 +169,8 @@ export async function POST(
|
|||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
results: [],
|
results: [],
|
||||||
|
truncated: false,
|
||||||
|
rawCount: 0,
|
||||||
message: 'No torrents/nzbs found',
|
message: 'No torrents/nzbs found',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -214,6 +216,14 @@ export async function POST(
|
|||||||
// User can see scores and make their own decision
|
// User can see scores and make their own decision
|
||||||
logger.debug(`Ranked ${rankedResults.length} results (no threshold filter - user decides)`);
|
logger.debug(`Ranked ${rankedResults.length} results (no threshold filter - user decides)`);
|
||||||
|
|
||||||
|
const truncated = rankedResults.length > MAX_RANKED_RESULTS;
|
||||||
|
const limitedRankedResults = rankedResults.slice(0, MAX_RANKED_RESULTS);
|
||||||
|
if (truncated) {
|
||||||
|
logger.info(`Interactive search truncated after ranking: returning top ${MAX_RANKED_RESULTS} of ${rankedResults.length} ranked results`, {
|
||||||
|
rawCount: results.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Log top 3 results with detailed score breakdown for debugging
|
// Log top 3 results with detailed score breakdown for debugging
|
||||||
const top3 = rankedResults.slice(0, 3);
|
const top3 = rankedResults.slice(0, 3);
|
||||||
if (top3.length > 0) {
|
if (top3.length > 0) {
|
||||||
@@ -245,7 +255,7 @@ export async function POST(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add rank position to each result
|
// Add rank position to each result
|
||||||
const resultsWithRank = rankedResults.map((result, index) => ({
|
const resultsWithRank = limitedRankedResults.map((result, index) => ({
|
||||||
...result,
|
...result,
|
||||||
rank: index + 1,
|
rank: index + 1,
|
||||||
}));
|
}));
|
||||||
@@ -253,8 +263,10 @@ export async function POST(
|
|||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
results: resultsWithRank,
|
results: resultsWithRank,
|
||||||
message: rankedResults.length > 0
|
truncated,
|
||||||
? `Found ${rankedResults.length} results`
|
rawCount: results.length,
|
||||||
|
message: limitedRankedResults.length > 0
|
||||||
|
? `Found ${limitedRankedResults.length} results`
|
||||||
: 'No results found',
|
: 'No results found',
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import { getLanguageForRegion } from '../constants/language-config';
|
|||||||
import { filterBlockedResults } from '../utils/filter-blocked-results';
|
import { filterBlockedResults } from '../utils/filter-blocked-results';
|
||||||
import type { AudibleRegion } from '../types/audible';
|
import type { AudibleRegion } from '../types/audible';
|
||||||
|
|
||||||
|
const MAX_RANKED_RESULTS = 100;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process search indexers job
|
* Process search indexers job
|
||||||
* Searches configured indexers for audiobook torrents
|
* Searches configured indexers for audiobook torrents
|
||||||
@@ -103,7 +105,6 @@ export async function processSearchIndexers(payload: SearchIndexersPayload): Pro
|
|||||||
categories: group.categories,
|
categories: group.categories,
|
||||||
indexerIds: group.indexerIds,
|
indexerIds: group.indexerIds,
|
||||||
minSeeders: 1, // Only torrents with at least 1 seeder
|
minSeeders: 1, // Only torrents with at least 1 seeder
|
||||||
maxResults: 100, // Limit per group
|
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info(`Group ${i + 1} returned ${groupResults.length} results`);
|
logger.info(`Group ${i + 1} returned ${groupResults.length} results`);
|
||||||
@@ -201,15 +202,20 @@ export async function processSearchIndexers(payload: SearchIndexersPayload): Pro
|
|||||||
// Dual threshold filtering:
|
// Dual threshold filtering:
|
||||||
// 1. Base score must be >= 50 (quality minimum)
|
// 1. Base score must be >= 50 (quality minimum)
|
||||||
// 2. Final score must be >= 50 (not disqualified by negative bonuses)
|
// 2. Final score must be >= 50 (not disqualified by negative bonuses)
|
||||||
const filteredResults = rankedResults.filter(result =>
|
const qualifyingResults = rankedResults.filter(result =>
|
||||||
result.score >= 50 && result.finalScore >= 50
|
result.score >= 50 && result.finalScore >= 50
|
||||||
);
|
);
|
||||||
|
const resultsTruncated = qualifyingResults.length > MAX_RANKED_RESULTS;
|
||||||
|
const filteredResults = qualifyingResults.slice(0, MAX_RANKED_RESULTS);
|
||||||
|
|
||||||
const disqualifiedByNegativeBonus = rankedResults.filter(result =>
|
const disqualifiedByNegativeBonus = rankedResults.filter(result =>
|
||||||
result.score >= 50 && result.finalScore < 50
|
result.score >= 50 && result.finalScore < 50
|
||||||
).length;
|
).length;
|
||||||
|
|
||||||
logger.info(`Ranked ${rankedResults.length} results, ${filteredResults.length} above threshold (50/100 base + final)`);
|
logger.info(`Ranked ${rankedResults.length} results, ${qualifyingResults.length} above threshold (50/100 base + final)`);
|
||||||
|
if (resultsTruncated) {
|
||||||
|
logger.info(`Limited automatic search to the top ${MAX_RANKED_RESULTS} of ${qualifyingResults.length} qualifying results after ranking`);
|
||||||
|
}
|
||||||
if (disqualifiedByNegativeBonus > 0) {
|
if (disqualifiedByNegativeBonus > 0) {
|
||||||
logger.info(`${disqualifiedByNegativeBonus} torrents disqualified by negative flag bonuses`);
|
logger.info(`${disqualifiedByNegativeBonus} torrents disqualified by negative flag bonuses`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ const rankTorrentsMock = vi.hoisted(() => vi.fn());
|
|||||||
const groupIndexersMock = vi.hoisted(() => vi.fn());
|
const groupIndexersMock = vi.hoisted(() => vi.fn());
|
||||||
const groupDescriptionMock = vi.hoisted(() => vi.fn(() => 'Group'));
|
const groupDescriptionMock = vi.hoisted(() => vi.fn(() => 'Group'));
|
||||||
|
|
||||||
|
vi.mock('@/lib/db', () => ({
|
||||||
|
prisma: {},
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock('@/lib/middleware/auth', () => ({
|
vi.mock('@/lib/middleware/auth', () => ({
|
||||||
requireAuth: requireAuthMock,
|
requireAuth: requireAuthMock,
|
||||||
}));
|
}));
|
||||||
@@ -94,6 +98,45 @@ describe('Audiobooks search torrents route', () => {
|
|||||||
expect(payload.results[0].rank).toBe(1);
|
expect(payload.results[0].rank).toBe(1);
|
||||||
expect(rankTorrentsMock).toHaveBeenCalled();
|
expect(rankTorrentsMock).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('ranks all candidates before returning the top 100 with truncation metadata', async () => {
|
||||||
|
authRequest.json.mockResolvedValue({ title: 'Title', author: 'Author' });
|
||||||
|
configServiceMock.get
|
||||||
|
.mockResolvedValueOnce(JSON.stringify([{ id: 1, name: 'Indexer', protocol: 'torrent', priority: 10 }]))
|
||||||
|
.mockResolvedValueOnce(null);
|
||||||
|
groupIndexersMock.mockReturnValue({ groups: [{ categories: [3030], indexerIds: [1] }], skippedIndexers: [] });
|
||||||
|
|
||||||
|
const candidates = Array.from({ length: 101 }, (_, index) => ({
|
||||||
|
title: index === 100 ? 'Best result' : `Result ${index}`,
|
||||||
|
size: 100,
|
||||||
|
indexer: 'Indexer',
|
||||||
|
indexerId: 1,
|
||||||
|
}));
|
||||||
|
const rankedCandidates = [candidates[100], ...candidates.slice(0, 100)].map((result, index) => ({
|
||||||
|
...result,
|
||||||
|
score: 100 - index,
|
||||||
|
breakdown: { matchScore: 50, formatScore: 0, sizeScore: 0, seederScore: 0, notes: [] },
|
||||||
|
bonusPoints: 0,
|
||||||
|
bonusModifiers: [],
|
||||||
|
finalScore: 100 - index,
|
||||||
|
}));
|
||||||
|
prowlarrMock.searchWithVariations.mockResolvedValue(candidates);
|
||||||
|
rankTorrentsMock.mockReturnValue(rankedCandidates);
|
||||||
|
|
||||||
|
const { POST } = await import('@/app/api/audiobooks/search-torrents/route');
|
||||||
|
const response = await POST({} as any);
|
||||||
|
const payload = await response.json();
|
||||||
|
|
||||||
|
expect(rankTorrentsMock.mock.calls[0][0]).toHaveLength(101);
|
||||||
|
expect(prowlarrMock.searchWithVariations).toHaveBeenCalledWith('Title', 'Author', {
|
||||||
|
categories: [3030],
|
||||||
|
indexerIds: [1],
|
||||||
|
});
|
||||||
|
expect(payload.results).toHaveLength(100);
|
||||||
|
expect(payload.results[0]).toEqual(expect.objectContaining({ title: 'Best result', rank: 1 }));
|
||||||
|
expect(payload.truncated).toBe(true);
|
||||||
|
expect(payload.rawCount).toBe(101);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -153,6 +153,51 @@ describe('Request action routes', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('ranks the complete interactive result set before returning the top 100', async () => {
|
||||||
|
authRequest.json.mockResolvedValue({});
|
||||||
|
prismaMock.request.findUnique.mockResolvedValueOnce({
|
||||||
|
id: 'req-complete-ranking',
|
||||||
|
userId: 'user-1',
|
||||||
|
audiobook: { title: 'Title', author: 'Author', audibleAsin: null },
|
||||||
|
});
|
||||||
|
prismaMock.user.findUnique.mockResolvedValueOnce({
|
||||||
|
role: 'user',
|
||||||
|
interactiveSearchAccess: null,
|
||||||
|
});
|
||||||
|
configServiceMock.get.mockResolvedValueOnce(JSON.stringify([{ id: 1, priority: 10, categories: [3030] }]));
|
||||||
|
configServiceMock.get.mockResolvedValueOnce(null);
|
||||||
|
groupIndexersMock.mockReturnValue({ groups: [{ categories: [3030], indexerIds: [1] }], skippedIndexers: [] });
|
||||||
|
|
||||||
|
const candidates = Array.from({ length: 101 }, (_, index) => ({
|
||||||
|
title: index === 100 ? 'Best result' : `Result ${index}`,
|
||||||
|
size: 100,
|
||||||
|
}));
|
||||||
|
const rankedCandidates = [candidates[100], ...candidates.slice(0, 100)].map((result, index) => ({
|
||||||
|
...result,
|
||||||
|
score: 100 - index,
|
||||||
|
breakdown: { matchScore: 50, formatScore: 0, sizeScore: 0, seederScore: 0, notes: [] },
|
||||||
|
bonusPoints: 0,
|
||||||
|
bonusModifiers: [],
|
||||||
|
finalScore: 100 - index,
|
||||||
|
}));
|
||||||
|
prowlarrMock.searchWithVariations.mockResolvedValueOnce(candidates);
|
||||||
|
rankTorrentsMock.mockReturnValueOnce(rankedCandidates);
|
||||||
|
|
||||||
|
const { POST } = await import('@/app/api/requests/[id]/interactive-search/route');
|
||||||
|
const response = await POST({} as any, { params: Promise.resolve({ id: 'req-complete-ranking' }) });
|
||||||
|
const payload = await response.json();
|
||||||
|
|
||||||
|
expect(rankTorrentsMock.mock.calls[0][0]).toHaveLength(101);
|
||||||
|
expect(prowlarrMock.searchWithVariations).toHaveBeenCalledWith('Title', 'Author', {
|
||||||
|
categories: [3030],
|
||||||
|
indexerIds: [1],
|
||||||
|
});
|
||||||
|
expect(payload.results).toHaveLength(100);
|
||||||
|
expect(payload.results[0]).toEqual(expect.objectContaining({ title: 'Best result', rank: 1 }));
|
||||||
|
expect(payload.truncated).toBe(true);
|
||||||
|
expect(payload.rawCount).toBe(101);
|
||||||
|
});
|
||||||
|
|
||||||
it('performs interactive search gracefully when runtime fetch fails', async () => {
|
it('performs interactive search gracefully when runtime fetch fails', async () => {
|
||||||
authRequest.json.mockResolvedValue({});
|
authRequest.json.mockResolvedValue({});
|
||||||
prismaMock.request.findUnique.mockResolvedValueOnce({
|
prismaMock.request.findUnique.mockResolvedValueOnce({
|
||||||
|
|||||||
@@ -117,6 +117,52 @@ describe('processSearchIndexers', () => {
|
|||||||
expect.objectContaining({ title: 'Book - Author' }),
|
expect.objectContaining({ title: 'Book - Author' }),
|
||||||
[expect.objectContaining({ title: 'Book - Author - Alternate' })]
|
[expect.objectContaining({ title: 'Book - Author - Alternate' })]
|
||||||
);
|
);
|
||||||
|
expect(prowlarrMock.searchWithVariations).toHaveBeenCalledWith('Book', 'Author', {
|
||||||
|
categories: [3030],
|
||||||
|
indexerIds: [1],
|
||||||
|
minSeeders: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ranks all candidates before limiting automatic search to 100 results', async () => {
|
||||||
|
configMock.get.mockImplementation(async (key: string) => {
|
||||||
|
if (key === 'prowlarr_indexers') {
|
||||||
|
return JSON.stringify([{ id: 1, name: 'Indexer', protocol: 'torrent', priority: 10, categories: [3030] }]);
|
||||||
|
}
|
||||||
|
if (key === 'indexer_flag_config') return JSON.stringify([]);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const candidates = Array.from({ length: 101 }, (_, index) => ({
|
||||||
|
indexer: 'Indexer',
|
||||||
|
indexerId: 1,
|
||||||
|
title: 'Book - Author',
|
||||||
|
size: 50 * 1024 * 1024,
|
||||||
|
seeders: index === 100 ? 500 : 1,
|
||||||
|
publishDate: new Date(),
|
||||||
|
downloadUrl: `magnet:?xt=urn:btih:${index}`,
|
||||||
|
guid: `guid-${index}`,
|
||||||
|
format: 'M4B',
|
||||||
|
}));
|
||||||
|
prowlarrMock.searchWithVariations.mockResolvedValue(candidates);
|
||||||
|
prismaMock.request.update.mockResolvedValue({});
|
||||||
|
|
||||||
|
const { processSearchIndexers } = await import('@/lib/processors/search-indexers.processor');
|
||||||
|
const result = await processSearchIndexers({
|
||||||
|
requestId: 'req-rank-before-limit',
|
||||||
|
audiobook: { id: 'a-rank-before-limit', title: 'Book', author: 'Author' },
|
||||||
|
jobId: 'job-rank-before-limit',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(jobQueueMock.addDownloadJob).toHaveBeenCalledWith(
|
||||||
|
'req-rank-before-limit',
|
||||||
|
expect.anything(),
|
||||||
|
expect.objectContaining({ guid: 'guid-100' }),
|
||||||
|
expect.any(Array)
|
||||||
|
);
|
||||||
|
const fallbackResults = jobQueueMock.addDownloadJob.mock.calls[0][3];
|
||||||
|
expect(fallbackResults).toHaveLength(99);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fails when no indexers are configured', async () => {
|
it('fails when no indexers are configured', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user