From 981185e56fa863969a9cbbecde2e3ef513bcd7bf Mon Sep 17 00:00:00 2001 From: Flaminel Date: Tue, 27 Oct 2026 12:06:44 +0200 Subject: [PATCH] added whisparr v2 seeker e2e tests --- .../live-arr-fast/search-grab.api.spec.ts | 5 +- .../live-arr-fast/seeker-filters.api.spec.ts | 53 +++++++++++ .../live-arr-fast/seeker-preview.api.spec.ts | 46 ++++++++++ .../seeker-search-mode.api.spec.ts | 90 +++++++++++++++++-- e2e/tests/seeker/seeker-config.api.spec.ts | 9 +- 5 files changed, 190 insertions(+), 13 deletions(-) diff --git a/e2e/tests/live-arr-fast/search-grab.api.spec.ts b/e2e/tests/live-arr-fast/search-grab.api.spec.ts index 70509514..eecb731b 100644 --- a/e2e/tests/live-arr-fast/search-grab.api.spec.ts +++ b/e2e/tests/live-arr-fast/search-grab.api.spec.ts @@ -5,6 +5,7 @@ import { RADARR, SONARR, WHISPARR, + WHISPARR2, TRANSITION_TIMEOUT, TRIGGERED_SEARCH_TIMEOUT, arrangeInstance, @@ -44,8 +45,8 @@ test.describe('Seeker grab on demand', () => { await resetLiveArrState(); }); - for (const target of [SONARR, RADARR, WHISPARR, LIDARR]) { - test(`records the real ${target.type} grab on the search event`, async ({ api }) => { + for (const target of [SONARR, RADARR, WHISPARR, WHISPARR2, LIDARR]) { + test(`records the real ${target.type} v${target.version} grab on the search event`, async ({ api }) => { test.setTimeout(TEST_TIMEOUT); const startedAt = new Date().toISOString(); diff --git a/e2e/tests/live-arr-fast/seeker-filters.api.spec.ts b/e2e/tests/live-arr-fast/seeker-filters.api.spec.ts index 37e81c06..c757d279 100644 --- a/e2e/tests/live-arr-fast/seeker-filters.api.spec.ts +++ b/e2e/tests/live-arr-fast/seeker-filters.api.spec.ts @@ -6,6 +6,7 @@ import { RADARR, SONARR, WHISPARR, + WHISPARR2, addDecoyDownloads, arrangeInstance, arrangeQueueCleaner, @@ -528,6 +529,58 @@ test.describe('Seeker candidate filters on Whisparr v3', () => { }); }); +test.describe('Seeker candidate filters on Whisparr v2', () => { + let library: Array> = []; + let scenes: Array<{ id: number; monitored: boolean }> = []; + + test.beforeEach(async () => { + await resetLiveArrState(); + library = await snapshotLibrary(WHISPARR2); + scenes = await WHISPARR2.arr.get>( + `/api/v3/episode?seriesId=${WHISPARR2.itemId}`, + ); + await indexerMock.stub(torznabSearchStub(WHISPARR2.searchMode, [])); + }); + + test.afterEach(async ({ api }) => { + await teardownInstances(api); + for (const monitored of [true, false]) { + const episodeIds = scenes.filter((scene) => scene.monitored === monitored).map((scene) => scene.id); + if (episodeIds.length > 0) { + await WHISPARR2.arr.put('/api/v3/episode/monitor', { episodeIds, monitored }); + } + } + await restoreLibrary(WHISPARR2, library); + await resetLiveArrState(); + }); + + test('searches the seeded site when nothing filters it out', async ({ api }) => { + const instanceId = await arrangeInstance(api, WHISPARR2); + await triggerSeeker(api); + + await expect.poll(async () => (await listSearchEvents(api, instanceId)).length, { timeout: SETTLE_MS }).toBe(1); + + const event = await firstSearchEvent(api, instanceId); + expect(event?.itemTitle).toContain(WHISPARR2.seededTitle); + }); + + test('skips a site carrying a skipped tag', async ({ api }) => { + const tagId = await WHISPARR2.arr.ensureTag(SKIP_TAG); + const instanceId = await arrangeInstance(api, WHISPARR2, { instance: { skipTags: [SKIP_TAG] } }); + + const site = await WHISPARR2.arr.get>(`/api/v3/series/${WHISPARR2.itemId}`); + await WHISPARR2.arr.put(`/api/v3/series/${WHISPARR2.itemId}`, { ...site, tags: [tagId] }); + await expectNoSearch(api, instanceId, SETTLE_MS); + }); + + test('skips the site while every scene is unmonitored and monitoredOnly is on', async ({ api }) => { + const instanceId = await arrangeInstance(api, WHISPARR2, { instance: { monitoredOnly: true } }); + + await WHISPARR2.arr.put('/api/v3/episode/monitor', { episodeIds: scenes.map((scene) => scene.id), monitored: false }); + await expectNoSearch(api, instanceId, SETTLE_MS); + }); +}); + test.describe('Seeker candidate filters on Lidarr', () => { let library: Array> = []; let albums: Array> = []; diff --git a/e2e/tests/live-arr-fast/seeker-preview.api.spec.ts b/e2e/tests/live-arr-fast/seeker-preview.api.spec.ts index 26486d9d..4bef9b01 100644 --- a/e2e/tests/live-arr-fast/seeker-preview.api.spec.ts +++ b/e2e/tests/live-arr-fast/seeker-preview.api.spec.ts @@ -7,6 +7,7 @@ import { RUN_TAG, SONARR, WHISPARR, + WHISPARR2, TRIGGERED_SEARCH_TIMEOUT, arrangeInstance, listSearchEvents, @@ -203,6 +204,51 @@ test.describe('Seeker preview on Whisparr v3', () => { }); }); +test.describe('Seeker preview on Whisparr v2', () => { + const { yearSearch, singleScene } = TEST_CONFIG.whisparr2Seed; + + let library: Array> = []; + + test.beforeEach(async () => { + await resetLiveArrState(); + library = await snapshotLibrary(WHISPARR2); + await indexerMock.stub(torznabSearchStub(WHISPARR2.searchMode, [])); + }); + + test.afterEach(async ({ api }) => { + await teardownInstances(api); + await WHISPARR2.arr.put('/api/v3/episode/monitor', { + episodeIds: yearSearch.scenes.map((scene) => scene.id), + monitored: true, + }); + await restoreLibrary(WHISPARR2, library); + await resetLiveArrState(); + }); + + test('lists the seeded site by year and scene dates with funnel counts', async ({ api }) => { + const instanceId = await arrangeInstance(api, WHISPARR2); + + const preview = await getPreview(api, instanceId); + + expect(preview.nextUp.map((u) => u.name)).toContain(`${WHISPARR2.seededTitle} ${yearSearch.year}`); + expect(preview.funnel.libraryItems).toBe(library.length); + expect(preview.funnel.eligibleUnits).toBeGreaterThan(0); + expect(await listSearchEvents(api, instanceId)).toHaveLength(0); + }); + + test('lists a single scene by its release date once the year is not fully wanted', async ({ api }) => { + await WHISPARR2.arr.put('/api/v3/episode/monitor', { + episodeIds: yearSearch.scenes.map((scene) => scene.id), + monitored: false, + }); + const instanceId = await arrangeInstance(api, WHISPARR2); + + const preview = await getPreview(api, instanceId); + + expect(preview.nextUp.map((u) => u.name)).toContain(`${WHISPARR2.seededTitle} ${singleScene.monitored.releaseDate}`); + }); +}); + test.describe('Seeker preview on Lidarr', () => { let library: Array> = []; diff --git a/e2e/tests/live-arr-fast/seeker-search-mode.api.spec.ts b/e2e/tests/live-arr-fast/seeker-search-mode.api.spec.ts index cf47bcf6..ea49d5be 100644 --- a/e2e/tests/live-arr-fast/seeker-search-mode.api.spec.ts +++ b/e2e/tests/live-arr-fast/seeker-search-mode.api.spec.ts @@ -1,10 +1,12 @@ import { rmSync, truncateSync } from 'node:fs'; import { join, resolve } from 'node:path'; -import { test, expect } from '../fixtures/base'; +import { test, expect, TEST_CONFIG } from '../fixtures/base'; import type { CleanuparrApi } from '../helpers/api'; import { indexerMock, type LiveArrCommand } from '../helpers/live-arr'; import { SONARR, + WHISPARR2, + type SeededArr, arrangeInstance, listSearchEvents, resetLiveArrState, @@ -41,13 +43,13 @@ interface Episode { monitored: boolean; } -async function searchCommandsAfter(lastId: number): Promise { - const commands = await SONARR.arr.commands(); +async function searchCommandsAfter(lastId: number, target: SeededArr = SONARR): Promise { + const commands = await target.arr.commands(); return commands.filter((c) => c.id > lastId && (c.name === 'EpisodeSearch' || c.name === 'SeasonSearch')); } -async function lastCommandId(): Promise { - const commands = await SONARR.arr.commands(); +async function lastCommandId(target: SeededArr = SONARR): Promise { + const commands = await target.arr.commands(); return Math.max(0, ...commands.map((c) => c.id)); } @@ -102,14 +104,18 @@ async function waitForIdleSonarr(): Promise { } /** Runs the Seeker once and returns the one search command it sent. */ -async function searchOnce(api: CleanuparrApi, instanceId: string): Promise { - const before = await lastCommandId(); +async function searchOnce( + api: CleanuparrApi, + instanceId: string, + target: SeededArr = SONARR, +): Promise { + const before = await lastCommandId(target); await triggerSeeker(api); await expect.poll(async () => (await listSearchEvents(api, instanceId)).length, { timeout: SETTLE_MS }).toBe(1); - await expect.poll(async () => (await searchCommandsAfter(before)).length, { timeout: SETTLE_MS }).toBe(1); - return (await searchCommandsAfter(before))[0]; + await expect.poll(async () => (await searchCommandsAfter(before, target)).length, { timeout: SETTLE_MS }).toBe(1); + return (await searchCommandsAfter(before, target))[0]; } test.describe('Seeker Sonarr search mode', () => { @@ -181,3 +187,69 @@ test.describe('Seeker Sonarr search mode', () => { expect(command.body?.episodeIds).toEqual([findEpisode(episodes, 2).id]); }); }); + +test.describe('Seeker Whisparr v2 search mode', () => { + const { yearSearch, singleScene } = TEST_CONFIG.whisparr2Seed; + + let library: Array>; + let scenes: Array<{ id: number; monitored: boolean }>; + + async function setMonitored(sceneIds: number[], monitored: boolean): Promise { + await WHISPARR2.arr.put('/api/v3/episode/monitor', { episodeIds: sceneIds, monitored }); + } + + test.beforeEach(async () => { + await resetLiveArrState(); + await indexerMock.stub(torznabSearchStub(WHISPARR2.searchMode, [])); + library = await snapshotLibrary(WHISPARR2); + scenes = await WHISPARR2.arr.get>( + `/api/v3/episode?seriesId=${WHISPARR2.itemId}`, + ); + }); + + test.afterEach(async ({ api }) => { + await teardownInstances(api); + for (const monitored of [true, false]) { + const sceneIds = scenes.filter((scene) => scene.monitored === monitored).map((scene) => scene.id); + if (sceneIds.length > 0) { + await setMonitored(sceneIds, monitored); + } + } + await restoreLibrary(WHISPARR2, library); + await resetLiveArrState(); + }); + + test('auto searches a fully wanted year as a whole', async ({ api }) => { + test.setTimeout(180_000); + await setMonitored([singleScene.monitored.id], false); + + const instanceId = await arrangeInstance(api, WHISPARR2, { instance: { sonarrSearchMode: 'Auto' } }); + const command = await searchOnce(api, instanceId, WHISPARR2); + + expect(command.name).toBe('SeasonSearch'); + expect(command.body?.seasonNumber).toBe(yearSearch.year); + }); + + test('auto searches a partly wanted year one scene at a time', async ({ api }) => { + test.setTimeout(180_000); + await setMonitored(yearSearch.scenes.map((scene) => scene.id), false); + + const instanceId = await arrangeInstance(api, WHISPARR2, { instance: { sonarrSearchMode: 'Auto' } }); + const command = await searchOnce(api, instanceId, WHISPARR2); + + expect(command.name).toBe('EpisodeSearch'); + expect(command.body?.episodeIds).toEqual([singleScene.monitored.id]); + }); + + test('episode mode searches a fully wanted year one scene at a time', async ({ api }) => { + test.setTimeout(180_000); + await setMonitored([singleScene.monitored.id], false); + + const instanceId = await arrangeInstance(api, WHISPARR2, { instance: { sonarrSearchMode: 'Episode' } }); + const command = await searchOnce(api, instanceId, WHISPARR2); + + expect(command.name).toBe('EpisodeSearch'); + expect(command.body?.episodeIds).toHaveLength(1); + expect(yearSearch.scenes.map((scene) => scene.id)).toContain(command.body?.episodeIds?.[0]); + }); +}); diff --git a/e2e/tests/seeker/seeker-config.api.spec.ts b/e2e/tests/seeker/seeker-config.api.spec.ts index b573e0fe..e3fcc756 100644 --- a/e2e/tests/seeker/seeker-config.api.spec.ts +++ b/e2e/tests/seeker/seeker-config.api.spec.ts @@ -21,7 +21,7 @@ test.describe.serial('Seeker — config', () => { expect(body).not.toHaveProperty('useCustomFormatScore'); }); - test('lists a Whisparr v3 instance but not a v2 one', async ({ api, mocks }) => { + test('lists Whisparr v2 and v3 instances', async ({ api, mocks }) => { await mocks.arr.stub(ArrStubs.arrHealthStub()); const created: string[] = []; @@ -42,8 +42,13 @@ test.describe.serial('Seeker — config', () => { const config = await (await api.seeker.getConfig()).json(); const listed = config.instances.map((i: { arrInstanceId: string }) => i.arrInstanceId); + expect(listed).toContain(v2Id); expect(listed).toContain(v3Id); - expect(listed).not.toContain(v2Id); + + const versionOf = (id: string) => + config.instances.find((i: { arrInstanceId: string }) => i.arrInstanceId === id).instanceVersion; + expect(versionOf(v2Id)).toBe(2); + expect(versionOf(v3Id)).toBe(3); } finally { for (const id of created) { await api.arr.deleteInstance('whisparr', id);