test: Integrate fetchPlaylists into scheduler checkChannel as best-effo…
- "src/services/scheduler.ts" - "src/__tests__/scheduler.test.ts" GSD-Task: S03/T01
This commit is contained in:
parent
ed901c8240
commit
8ebacba3e1
2 changed files with 127 additions and 0 deletions
|
|
@ -33,9 +33,11 @@ import type {
|
|||
Channel,
|
||||
PlatformSourceMetadata,
|
||||
PlatformContentMetadata,
|
||||
PlaylistDiscoveryResult,
|
||||
} from '../types/index';
|
||||
import type { LibSQLDatabase } from 'drizzle-orm/libsql';
|
||||
import type * as schema from '../db/schema/index';
|
||||
import { getPlaylistsByChannelId } from '../db/repositories/playlist-repository';
|
||||
|
||||
// ── Rate Limiter Tests ──
|
||||
|
||||
|
|
@ -891,6 +893,91 @@ describe('SchedulerService', () => {
|
|||
|
||||
scheduler.stop();
|
||||
});
|
||||
|
||||
it('calls fetchPlaylists after successful scan when source supports it', async () => {
|
||||
const channel = await insertTestChannel();
|
||||
|
||||
// Build a registry with fetchPlaylists support
|
||||
const fetchPlaylistsFn = vi.fn<(ch: Channel) => Promise<PlaylistDiscoveryResult[]>>()
|
||||
.mockResolvedValueOnce([
|
||||
{
|
||||
platformPlaylistId: 'PL_test_1',
|
||||
title: 'Test Playlist',
|
||||
videoIds: [],
|
||||
},
|
||||
]);
|
||||
const mockSourceWithPlaylists: PlatformSource = {
|
||||
resolveChannel: mockResolveChannel,
|
||||
fetchRecentContent: mockFetchRecentContent,
|
||||
fetchPlaylists: fetchPlaylistsFn,
|
||||
};
|
||||
const reg = new PlatformRegistry();
|
||||
reg.register(Platform.YouTube, mockSourceWithPlaylists);
|
||||
|
||||
const localLimiter = new RateLimiter({
|
||||
[Platform.YouTube]: { minIntervalMs: 0 },
|
||||
});
|
||||
const scheduler = new SchedulerService(db, reg, localLimiter);
|
||||
|
||||
mockFetchRecentContent.mockResolvedValueOnce(makeCannedContent(1, `plsync_${channel.id}`));
|
||||
const result = await scheduler.checkChannel(channel);
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
// Allow background sidecar to complete
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
expect(fetchPlaylistsFn).toHaveBeenCalledOnce();
|
||||
|
||||
// Verify playlist was persisted
|
||||
const playlists = await getPlaylistsByChannelId(db, channel.id);
|
||||
expect(playlists.length).toBe(1);
|
||||
expect(playlists[0].title).toBe('Test Playlist');
|
||||
|
||||
scheduler.stop();
|
||||
});
|
||||
|
||||
it('scan succeeds even when playlist sync throws', async () => {
|
||||
const channel = await insertTestChannel();
|
||||
|
||||
const fetchPlaylistsFn = vi.fn<(ch: Channel) => Promise<PlaylistDiscoveryResult[]>>()
|
||||
.mockRejectedValueOnce(new Error('Playlist fetch exploded'));
|
||||
const mockSourceWithPlaylists: PlatformSource = {
|
||||
resolveChannel: mockResolveChannel,
|
||||
fetchRecentContent: mockFetchRecentContent,
|
||||
fetchPlaylists: fetchPlaylistsFn,
|
||||
};
|
||||
const reg = new PlatformRegistry();
|
||||
reg.register(Platform.YouTube, mockSourceWithPlaylists);
|
||||
|
||||
const localLimiter = new RateLimiter({
|
||||
[Platform.YouTube]: { minIntervalMs: 0 },
|
||||
});
|
||||
const scheduler = new SchedulerService(db, reg, localLimiter);
|
||||
|
||||
mockFetchRecentContent.mockResolvedValueOnce(makeCannedContent(2, `plfail_${channel.id}`));
|
||||
const result = await scheduler.checkChannel(channel);
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
expect(result.newItems).toBe(2);
|
||||
// Allow background sidecar to attempt and fail
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
expect(fetchPlaylistsFn).toHaveBeenCalledOnce();
|
||||
|
||||
scheduler.stop();
|
||||
});
|
||||
|
||||
it('skips playlist sync when source does not support fetchPlaylists', async () => {
|
||||
const channel = await insertTestChannel();
|
||||
const scheduler = new SchedulerService(db, registry, rateLimiter);
|
||||
|
||||
// Default mock source has no fetchPlaylists
|
||||
mockFetchRecentContent.mockResolvedValueOnce(makeCannedContent(1, `noplsync_${channel.id}`));
|
||||
const result = await scheduler.checkChannel(channel);
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
expect(result.newItems).toBe(1);
|
||||
|
||||
scheduler.stop();
|
||||
});
|
||||
});
|
||||
|
||||
// ── addChannel() / removeChannel() ──
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
updateContentItem,
|
||||
} from '../db/repositories/content-repository';
|
||||
import { getPlatformSettings } from '../db/repositories/platform-settings-repository';
|
||||
import { upsertPlaylists } from '../db/repositories/playlist-repository';
|
||||
|
||||
// ── Types ──
|
||||
|
||||
|
|
@ -330,6 +331,18 @@ export class SchedulerService {
|
|||
});
|
||||
}
|
||||
|
||||
// 10. Best-effort playlist sync (K011 sidecar pattern)
|
||||
// Runs after content scan succeeds — failure never affects the scan result.
|
||||
if (source.fetchPlaylists && !effectiveSignal.aborted) {
|
||||
this.syncPlaylists(channel, source)
|
||||
.catch((err) => {
|
||||
console.error(
|
||||
`[scheduler] Playlist sync failed for channel ${channel.id}:`,
|
||||
err instanceof Error ? err.message : err
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
channelId: channel.id,
|
||||
channelName: channel.name,
|
||||
|
|
@ -517,6 +530,33 @@ export class SchedulerService {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Best-effort playlist sync for a channel.
|
||||
* Fetches playlists from the platform source and upserts them into the DB.
|
||||
* Failure is logged but never propagated (K011 sidecar pattern).
|
||||
*/
|
||||
private async syncPlaylists(channel: Channel, source: PlatformSource): Promise<void> {
|
||||
if (!source.fetchPlaylists) return;
|
||||
|
||||
try {
|
||||
const discoveryResults = await source.fetchPlaylists(channel);
|
||||
if (discoveryResults.length === 0) {
|
||||
console.log(`[scheduler] Playlist sync: no playlists found for channel ${channel.id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const upserted = await upsertPlaylists(this.db, channel.id, discoveryResults);
|
||||
console.log(
|
||||
`[scheduler] Playlist sync complete for channel ${channel.id}: ${upserted.length} playlists synced`
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`[scheduler] Playlist sync error for channel ${channel.id}:`,
|
||||
err instanceof Error ? err.message : err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Internal ──
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue