added live e2e for seeker recent grabs

This commit is contained in:
Flaminel committed 2026-10-20 15:55:52 +03:00
1 parent a3441376f4
commit 031fbaf1bf
3 files changed
+100 -1

No files matched your search

+26 -1
View File
@@ -159,7 +159,15 @@ export async function arrangeInstance(
const instances = config.instances.map((i: { arrInstanceId: string }) =>
i.arrInstanceId === instance.id
? { ...i, enabled: true, monitoredOnly: true, minCycleTimeDays: 0, ...overrides.instance }
? {
...i,
enabled: true,
monitoredOnly: true,
minCycleTimeDays: 0,
// Grabs from earlier specs stay in the arr's history for the whole suite run.
recentGrabWindowHours: 0,
...overrides.instance,
}
: i,
);
@@ -194,6 +202,23 @@ export async function arrangeInstance(
return instance.id;
}
/** Patches one already-arranged instance's settings and asserts the update stuck. */
export async function updateInstance(
api: CleanuparrApi,
instanceId: string,
patch: Record<string, unknown>,
): Promise<void> {
const config = await (await api.seeker.getConfig()).json();
const instances = config.instances.map((i: { arrInstanceId: string }) =>
i.arrInstanceId === instanceId ? { ...i, ...patch } : i,
);
const updated = await api.seeker.updateConfig({ ...config, instances });
if (!updated.ok) {
throw new Error(`Seeker config update failed: ${await updated.text()}`);
}
}
/** Removes the instances and restores the settings the specs changed. */
export async function teardownInstances(api: CleanuparrApi): Promise<void> {
for (const instance of createdInstances.splice(0)) {
@@ -11,14 +11,17 @@ import {
expectNoSearch,
firstSearchEvent,
listSearchEvents,
resetDownloads,
resetLiveArrState,
restoreLibrary,
snapshotLibrary,
teardownInstances,
teardownQueueCleaner,
triggerSeeker,
updateInstance,
RUN_TAG,
TRIGGERED_SEARCH_TIMEOUT,
type SeededArr,
} from '../helpers/seeker-live';
import { grabbableRelease, grabbableReleases } from '../helpers/mocks/torznab-stubs';
@@ -107,6 +110,27 @@ test.describe('Seeker candidate filters', () => {
throw new Error('the queue cleaner never struck the grabbed download');
}
/**
* Triggers one search and waits for the arr to record its grab.
*
* History, not the queue: the arr can refuse to track a release an earlier test grabbed.
*/
async function grabOnce(api: CleanuparrApi, instanceId: string, target: SeededArr): Promise<void> {
const since = encodeURIComponent(new Date().toISOString());
await triggerSeeker(api);
await expect
.poll(async () => (await listSearchEvents(api, instanceId)).length, { timeout: SETTLE_MS })
.toBe(1);
await expect
.poll(
async () => (await target.arr.get<unknown[]>(`/api/v3/history/since?date=${since}&eventType=1`)).length,
{ timeout: TRIGGERED_SEARCH_TIMEOUT },
)
.toBeGreaterThan(0);
}
/** Whether a strike landed within the window, without failing the test if none did. */
function pollStrikeCount(api: CleanuparrApi): Promise<boolean> {
return expect
@@ -281,4 +305,49 @@ test.describe('Seeker candidate filters', () => {
)
.toBe(2);
});
test('skips a movie grabbed within the recent grab window', async ({ api }) => {
test.setTimeout(180_000);
const instanceId = await arrangeInstance(api, RADARR, { instance: { monitoredOnly: true } });
await grabOnce(api, instanceId, RADARR);
await resetDownloads();
await updateInstance(api, instanceId, { recentGrabWindowHours: 24 });
await triggerSeeker(api);
await new Promise((resolve) => setTimeout(resolve, SETTLE_MS));
// The grab stays in arr history after resetDownloads clears the queue, so it is still skipped.
expect(await listSearchEvents(api, instanceId)).toHaveLength(1);
});
test('searches a grabbed movie again with the window off', async ({ api }) => {
test.setTimeout(180_000);
const instanceId = await arrangeInstance(api, RADARR, { instance: { monitoredOnly: true } });
await grabOnce(api, instanceId, RADARR);
await resetDownloads();
await triggerSeeker(api);
await expect
.poll(async () => (await listSearchEvents(api, instanceId)).length, { timeout: SETTLE_MS })
.toBe(2);
});
test('skips an episode grabbed within the recent grab window', async ({ api }) => {
test.setTimeout(180_000);
await indexerMock.stubMany(grabbableRelease(SONARR.searchMode, SONARR.release, SONARR.category).mappings);
const instanceId = await arrangeInstance(api, SONARR, { instance: { monitoredOnly: true } });
await grabOnce(api, instanceId, SONARR);
await resetDownloads();
await updateInstance(api, instanceId, { recentGrabWindowHours: 24 });
await triggerSeeker(api);
await new Promise((resolve) => setTimeout(resolve, SETTLE_MS));
expect(await listSearchEvents(api, instanceId)).toHaveLength(1);
});
});
@@ -43,6 +43,7 @@ test.describe('Seeker — per-instance config', () => {
expect(instance).toHaveProperty('enabled');
expect(instance).toHaveProperty('skipTags');
expect(instance).toHaveProperty('includeTags');
expect(instance).toHaveProperty('recentGrabWindowHours');
expect(instance).toHaveProperty('activeDownloadLimit');
expect(instance).toHaveProperty('ignoreStruckDownloads');
expect(instance).toHaveProperty('minCycleTimeDays');
@@ -59,6 +60,7 @@ test.describe('Seeker — per-instance config', () => {
expect(radarr.useCustomFormatScore).toBe(false);
expect(radarr.ignoreStruckDownloads).toBe(false);
expect(radarr.includeTags).toEqual([]);
expect(radarr.recentGrabWindowHours).toBe(24);
});
test('updates per-instance settings independently', async ({ api }) => {
@@ -75,6 +77,7 @@ test.describe('Seeker — per-instance config', () => {
useCustomFormatScore: true,
ignoreStruckDownloads: true,
includeTags: ['wanted', '4k'],
recentGrabWindowHours: 48,
};
}
if (i.arrInstanceId === sonarrId) {
@@ -95,7 +98,9 @@ test.describe('Seeker — per-instance config', () => {
expect(radarr.useCustomFormatScore).toBe(true);
expect(radarr.ignoreStruckDownloads).toBe(true);
expect(radarr.includeTags).toEqual(['wanted', '4k']);
expect(radarr.recentGrabWindowHours).toBe(48);
expect(sonarr.includeTags).toEqual([]);
expect(sonarr.recentGrabWindowHours).toBe(24);
expect(sonarr.ignoreStruckDownloads).toBe(false);
expect(sonarr.monitoredOnly).toBe(true);
expect(sonarr.useCutoff).toBe(true);