added whisparr v2 seeker e2e tests

This commit is contained in:
Flaminel committed 2026-10-27 12:06:44 +02:00
1 parent e27ce94209
commit 981185e56f
5 files changed
+190 -13

No files matched your search

@@ -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();
@@ -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<Record<string, unknown>> = [];
let scenes: Array<{ id: number; monitored: boolean }> = [];
test.beforeEach(async () => {
await resetLiveArrState();
library = await snapshotLibrary(WHISPARR2);
scenes = await WHISPARR2.arr.get<Array<{ id: number; monitored: boolean }>>(
`/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<Record<string, unknown>>(`/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<Record<string, unknown>> = [];
let albums: Array<Record<string, unknown>> = [];
@@ -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<Record<string, unknown>> = [];
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<Record<string, unknown>> = [];
@@ -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<LiveArrCommand[]> {
const commands = await SONARR.arr.commands();
async function searchCommandsAfter(lastId: number, target: SeededArr = SONARR): Promise<LiveArrCommand[]> {
const commands = await target.arr.commands();
return commands.filter((c) => c.id > lastId && (c.name === 'EpisodeSearch' || c.name === 'SeasonSearch'));
}
async function lastCommandId(): Promise<number> {
const commands = await SONARR.arr.commands();
async function lastCommandId(target: SeededArr = SONARR): Promise<number> {
const commands = await target.arr.commands();
return Math.max(0, ...commands.map((c) => c.id));
}
@@ -102,14 +104,18 @@ async function waitForIdleSonarr(): Promise<void> {
}
/** Runs the Seeker once and returns the one search command it sent. */
async function searchOnce(api: CleanuparrApi, instanceId: string): Promise<LiveArrCommand> {
const before = await lastCommandId();
async function searchOnce(
api: CleanuparrApi,
instanceId: string,
target: SeededArr = SONARR,
): Promise<LiveArrCommand> {
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<Record<string, unknown>>;
let scenes: Array<{ id: number; monitored: boolean }>;
async function setMonitored(sceneIds: number[], monitored: boolean): Promise<void> {
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<Array<{ id: number; monitored: boolean }>>(
`/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]);
});
});
+7 -2
View File
@@ -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);