feat: run full refresh with EPG rematching
Run full live catalog refreshes through live source sync, complete channel analysis, EPG source refresh, and persisted EPG rematching while keeping check-new incremental. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
168
test/t38FullRebuildOrchestrationRegression.test.js
Normal file
168
test/t38FullRebuildOrchestrationRegression.test.js
Normal file
@ -0,0 +1,168 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const Database = require('better-sqlite3');
|
||||
const liveProjection = require('../server/services/liveProjectionService');
|
||||
const {
|
||||
LIVE_PROJECTION_COLUMNS,
|
||||
LIVE_PROJECTION_SCHEMA_SQL,
|
||||
ensureColumns
|
||||
} = require('../server/db/liveProjectionSchema');
|
||||
|
||||
const repoRoot = path.join(__dirname, '..');
|
||||
const routeSource = fs.readFileSync(path.join(repoRoot, 'server', 'routes', 'live.js'), 'utf8');
|
||||
const projectionSource = fs.readFileSync(path.join(repoRoot, 'server', 'services', 'liveProjectionService.js'), 'utf8');
|
||||
const syncSource = fs.readFileSync(path.join(repoRoot, 'server', 'services', 'syncService.js'), 'utf8');
|
||||
|
||||
function routeBlock(endpoint) {
|
||||
const marker = `router.post('${endpoint}'`;
|
||||
const start = routeSource.indexOf(marker);
|
||||
assert.notEqual(start, -1, `${endpoint} route should exist`);
|
||||
const next = routeSource.indexOf('\nrouter.', start + marker.length);
|
||||
return routeSource.slice(start, next === -1 ? undefined : next);
|
||||
}
|
||||
|
||||
function createProjectionDb() {
|
||||
const db = new Database(':memory:');
|
||||
db.exec(`
|
||||
CREATE TABLE categories (
|
||||
id TEXT PRIMARY KEY,
|
||||
source_id INTEGER NOT NULL,
|
||||
category_id TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
parent_id TEXT,
|
||||
is_hidden INTEGER DEFAULT 0,
|
||||
data JSON
|
||||
);
|
||||
CREATE TABLE playlist_items (
|
||||
id TEXT PRIMARY KEY,
|
||||
source_id INTEGER NOT NULL,
|
||||
item_id TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
category_id TEXT,
|
||||
parent_id TEXT,
|
||||
stream_icon TEXT,
|
||||
stream_url TEXT,
|
||||
container_extension TEXT,
|
||||
rating REAL,
|
||||
year TEXT,
|
||||
added_at TEXT,
|
||||
is_hidden INTEGER DEFAULT 0,
|
||||
is_favorite INTEGER DEFAULT 0,
|
||||
data JSON
|
||||
);
|
||||
CREATE TABLE favorites (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
source_id INTEGER NOT NULL,
|
||||
item_id TEXT NOT NULL,
|
||||
item_type TEXT NOT NULL
|
||||
);
|
||||
`);
|
||||
ensureColumns(db, 'playlist_items', LIVE_PROJECTION_COLUMNS);
|
||||
db.exec(LIVE_PROJECTION_SCHEMA_SQL);
|
||||
return db;
|
||||
}
|
||||
|
||||
function seedProjectedChannel(db) {
|
||||
db.prepare(`
|
||||
INSERT INTO categories (id, source_id, category_id, type, name)
|
||||
VALUES ('1:sports', 1, 'sports', 'live', 'Sports')
|
||||
`).run();
|
||||
db.prepare(`
|
||||
INSERT INTO playlist_items
|
||||
(id, source_id, item_id, type, name, category_id, stream_icon, stream_url, data,
|
||||
language_code, language_confidence, language_detector, language_input_hash)
|
||||
VALUES ('1:bein-fr', 1, 'bein-fr', 'live', '|FR| beIN Sports 1 HD', 'sports',
|
||||
'', 'https://streams.test/bein-fr.m3u8', '{"tvgId":"bein.fr"}',
|
||||
'fr', 0.99, 'laya', 'fixture-hash')
|
||||
`).run();
|
||||
liveProjection.rebuild(db);
|
||||
}
|
||||
|
||||
test('manual full rebuild route delegates to a complete orchestration service, not startRebuild directly', () => {
|
||||
const rebuildRoute = routeBlock('/projection/rebuild');
|
||||
assert.match(
|
||||
rebuildRoute,
|
||||
/(?:orchestr|full).*rebuild|rebuild.*(?:orchestr|full)/i,
|
||||
'POST /api/live/projection/rebuild should call a full rebuild orchestrator'
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
rebuildRoute,
|
||||
/liveProjection\.startRebuild\(/,
|
||||
'full rebuild route should not bypass orchestration with liveProjection.startRebuild'
|
||||
);
|
||||
});
|
||||
|
||||
test('full rebuild orchestration contract orders live refresh, analyzeAll, EPG refresh, then persisted EPG match rebuild', () => {
|
||||
const combinedSource = `${routeSource}\n${projectionSource}\n${syncSource}`;
|
||||
const liveRefresh = combinedSource.search(/refresh(?:Enabled)?Live|syncLive|syncLiveSources|refreshLiveSources/i);
|
||||
const analyzeAll = combinedSource.search(/analyzeAll\s*:\s*true/);
|
||||
const epgRefresh = combinedSource.search(/refresh(?:Enabled)?Epg|syncEpg|refreshEpgSources/i);
|
||||
const epgMatchRebuild = combinedSource.search(/rebuild(?:Persisted)?EpgMatch|persist(?:ed)?.*epg.*match|epg.*match.*persist/i);
|
||||
|
||||
assert.ok(liveRefresh >= 0, 'full rebuild should refresh enabled live sources first');
|
||||
assert.ok(analyzeAll > liveRefresh, 'full rebuild should analyze all live channels after live refresh');
|
||||
assert.ok(epgRefresh > analyzeAll, 'full rebuild should refresh enabled EPG sources after analyzeAll');
|
||||
assert.ok(epgMatchRebuild > epgRefresh, 'full rebuild should rebuild persisted EPG matches after EPG refresh');
|
||||
});
|
||||
|
||||
test('full rebuild orchestration does not use per-source queueRebuild or Xtream VOD/series sync', () => {
|
||||
const rebuildRelatedSource = `${routeBlock('/projection/rebuild')}\n${projectionSource}`;
|
||||
assert.match(
|
||||
rebuildRelatedSource,
|
||||
/refresh(?:Enabled)?Live|syncLive|syncLiveSources|refreshLiveSources/i,
|
||||
'full rebuild should expose a live-only source refresh step'
|
||||
);
|
||||
assert.doesNotMatch(rebuildRelatedSource, /queueRebuild\(/, 'full rebuild should not enqueue per-source projection rebuilds');
|
||||
assert.doesNotMatch(rebuildRelatedSource, /getVod(?:Categories|Streams)\(/, 'full rebuild should not sync Xtream VOD');
|
||||
assert.doesNotMatch(rebuildRelatedSource, /getSeries(?:Categories)?\(/, 'full rebuild should not sync Xtream series');
|
||||
});
|
||||
|
||||
test('/api/live/channels reads persisted EPG matches instead of opportunistic heavy matching', () => {
|
||||
const db = createProjectionDb();
|
||||
seedProjectedChannel(db);
|
||||
try {
|
||||
assert.doesNotThrow(() => liveProjection.getChannelPage({
|
||||
q: 'bein sports 1',
|
||||
epgSources: [{ id: 7, enabled: true, type: 'epg' }]
|
||||
}, db, {
|
||||
iptvOrgCatalog: { findMatch: () => null },
|
||||
epgMatching: {
|
||||
findBestMatch: () => {
|
||||
throw new Error('opportunistic EPG matching should not run while reading live channels');
|
||||
}
|
||||
}
|
||||
}));
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('check-new performs only incremental live analysis, without full EPG refresh or analyzeAll', () => {
|
||||
const checkNewRoute = routeBlock('/projection/check-new');
|
||||
const checkNewImplementation = projectionSource.slice(
|
||||
projectionSource.indexOf('function checkNewStreams'),
|
||||
projectionSource.indexOf('function getChannels')
|
||||
);
|
||||
const checkNewSource = `${checkNewRoute}\n${checkNewImplementation}`;
|
||||
assert.doesNotMatch(checkNewSource, /analyzeAll\s*:\s*true/, 'check-new must not analyze the full catalog');
|
||||
assert.doesNotMatch(checkNewSource, /\.syncAll\(/, 'check-new must not run global sync');
|
||||
assert.doesNotMatch(checkNewSource, /syncEpg|refresh(?:Enabled)?Epg/i, 'check-new must not refresh all EPG sources');
|
||||
});
|
||||
|
||||
test('EPG matches are persisted by logical channel and language', () => {
|
||||
assert.match(
|
||||
LIVE_PROJECTION_SCHEMA_SQL,
|
||||
/CREATE TABLE IF NOT EXISTS .*epg.*match/i,
|
||||
'projection schema should declare a persisted EPG match table'
|
||||
);
|
||||
assert.match(
|
||||
LIVE_PROJECTION_SCHEMA_SQL,
|
||||
/UNIQUE\s*\(\s*logical_channel_id\s*,\s*language_code\s*\)/i,
|
||||
'persisted EPG matches should be unique per logical channel and language'
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user