From 653f7248e58b72413be0d2cdf2dbbbe5cb1f1abb Mon Sep 17 00:00:00 2001 From: Flaminel Date: Mon, 19 Oct 2026 14:44:00 +0300 Subject: [PATCH] added content kind e2e tests --- e2e/tests/helpers/live-arr.ts | 1 + e2e/tests/helpers/seeker-live.ts | 1 + .../seeker-content-kinds.api.spec.ts | 149 ++++++++++++++++++ 3 files changed, 151 insertions(+) create mode 100644 e2e/tests/live-arr-fast/seeker-content-kinds.api.spec.ts diff --git a/e2e/tests/helpers/live-arr.ts b/e2e/tests/helpers/live-arr.ts index dccbd122..2750bc24 100644 --- a/e2e/tests/helpers/live-arr.ts +++ b/e2e/tests/helpers/live-arr.ts @@ -24,6 +24,7 @@ export interface LiveArrCommand { id: number; name: string; status: string; + body?: { seriesId?: number; seasonNumber?: number; episodeIds?: number[] }; } export class LiveArr { diff --git a/e2e/tests/helpers/seeker-live.ts b/e2e/tests/helpers/seeker-live.ts index f1a5afa7..1f48e670 100644 --- a/e2e/tests/helpers/seeker-live.ts +++ b/e2e/tests/helpers/seeker-live.ts @@ -107,6 +107,7 @@ export interface SearchEvent { isDryRun: boolean; grabbedItems: string[] | null; searchReason: string; + contentKind: string | null; } export async function listSearchEvents(api: CleanuparrApi, instanceId: string): Promise { diff --git a/e2e/tests/live-arr-fast/seeker-content-kinds.api.spec.ts b/e2e/tests/live-arr-fast/seeker-content-kinds.api.spec.ts new file mode 100644 index 00000000..a8e99442 --- /dev/null +++ b/e2e/tests/live-arr-fast/seeker-content-kinds.api.spec.ts @@ -0,0 +1,149 @@ +import { test, expect } from '../fixtures/base'; +import { indexerMock, type LiveArrCommand } from '../helpers/live-arr'; +import { + SONARR, + arrangeInstance, + listSearchEvents, + resetLiveArrState, + restoreLibrary, + snapshotLibrary, + teardownInstances, + triggerSeeker, +} from '../helpers/seeker-live'; +import { torznabSearchStub } from '../helpers/mocks/torznab-stubs'; + +/** + * How the Seeker shapes a Sonarr search for each content kind. + * + * Sonarr runs one indexer query per episode for a specials season and for an airing anime season. + * The Seeker searches those one episode at a time. + */ + +const SETTLE_MS = 20_000; + +/** Sherlock, added unmonitored by the seed for its aired specials. */ +const SPECIALS_SERIES_ID = 2; +/** "Unlocking Sherlock", aired 2010. */ +const SPECIAL_EPISODE_NUMBER = 2; + +interface SonarrEpisode { + id: number; + seasonNumber: number; + episodeNumber: number; +} + +async function searchCommandsAfter(lastId: number): Promise { + const commands = await SONARR.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(); + return Math.max(0, ...commands.map((c) => c.id)); +} + +/** + * Puts the seeded series type back once Sonarr's own commands finish. + * A refresh still running after the type change would save the old type over the restore. + */ +async function restoreSeriesType(seeded: Record): Promise { + await expect + .poll(async () => (await SONARR.arr.commands()).filter((c) => c.status === 'queued' || c.status === 'started').length, { + timeout: SETTLE_MS, + }) + .toBe(0); + + await SONARR.arr.put(`/api/v3/series/${seeded.id}`, seeded); + + const restored = await SONARR.arr.get<{ seriesType: string }>(`/api/v3/series/${seeded.id}`); + expect(restored.seriesType).toBe(seeded.seriesType); +} + +test.describe('Seeker Sonarr content kinds', () => { + let library: Array>; + + test.beforeEach(async () => { + await resetLiveArrState(); + await indexerMock.stub(torznabSearchStub(SONARR.searchMode, [])); + library = await snapshotLibrary(SONARR); + }); + + test.afterEach(async ({ api }) => { + await teardownInstances(api); + await restoreLibrary(SONARR, library); + await resetLiveArrState(); + }); + + test('searches a special by episode, never by season', async ({ api }) => { + test.setTimeout(180_000); + + const episodes = await SONARR.arr.get(`/api/v3/episode?seriesId=${SPECIALS_SERIES_ID}`); + const special = episodes.find((e) => e.seasonNumber === 0 && e.episodeNumber === SPECIAL_EPISODE_NUMBER); + expect(special, 'the seed lost the Sherlock special').toBeTruthy(); + + for (const series of library) { + await SONARR.arr.put(`/api/v3/series/${series.id}`, { ...series, monitored: series.id === SPECIALS_SERIES_ID }); + } + await SONARR.arr.put('/api/v3/episode/monitor', { episodeIds: [special!.id], monitored: true }); + + try { + const instanceId = await arrangeInstance(api, SONARR); + const before = await lastCommandId(); + + await triggerSeeker(api); + await expect.poll(async () => (await listSearchEvents(api, instanceId)).length, { timeout: SETTLE_MS }).toBe(1); + + const [event] = await listSearchEvents(api, instanceId); + expect(event.contentKind).toBe('Special'); + expect(event.itemTitle).toBe('Sherlock S00E02'); + + await expect.poll(async () => (await searchCommandsAfter(before)).length, { timeout: SETTLE_MS }).toBe(1); + const [command] = await searchCommandsAfter(before); + expect(command.name).toBe('EpisodeSearch'); + expect(command.body?.episodeIds).toEqual([special!.id]); + } finally { + await SONARR.arr.put('/api/v3/episode/monitor', { episodeIds: [special!.id], monitored: false }); + } + }); + + test('searches a fully aired anime season as a whole', async ({ api }) => { + test.setTimeout(180_000); + + const seeded = library.find((s) => s.id === SONARR.itemId)!; + await SONARR.arr.put(`/api/v3/series/${seeded.id}`, { ...seeded, seriesType: 'anime' }); + + try { + const instanceId = await arrangeInstance(api, SONARR); + const before = await lastCommandId(); + + await triggerSeeker(api); + await expect.poll(async () => (await listSearchEvents(api, instanceId)).length, { timeout: SETTLE_MS }).toBe(1); + + const [event] = await listSearchEvents(api, instanceId); + expect(event.contentKind).toBe('Anime'); + + await expect.poll(async () => (await searchCommandsAfter(before)).length, { timeout: SETTLE_MS }).toBe(1); + const [command] = await searchCommandsAfter(before); + expect(command.name).toBe('SeasonSearch'); + expect(command.body?.seasonNumber).toBe(1); + } finally { + await restoreSeriesType(seeded); + } + }); + + test('tags a regular season search as standard', async ({ api }) => { + test.setTimeout(180_000); + + const instanceId = await arrangeInstance(api, SONARR); + const before = await lastCommandId(); + + await triggerSeeker(api); + await expect.poll(async () => (await listSearchEvents(api, instanceId)).length, { timeout: SETTLE_MS }).toBe(1); + + const [event] = await listSearchEvents(api, instanceId); + expect(event.contentKind).toBe('Standard'); + + await expect.poll(async () => (await searchCommandsAfter(before)).length, { timeout: SETTLE_MS }).toBe(1); + expect((await searchCommandsAfter(before))[0].name).toBe('SeasonSearch'); + }); +});