test: cover functional live detection chain with Laya fallback (#18)
This commit is contained in:
166
test/liveDetectionFunctionalChain.test.js
Normal file
166
test/liveDetectionFunctionalChain.test.js
Normal file
@ -0,0 +1,166 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const Database = require('better-sqlite3');
|
||||
const {
|
||||
ensureColumns,
|
||||
LIVE_PROJECTION_COLUMNS,
|
||||
LIVE_PROJECTION_SCHEMA_SQL
|
||||
} = require('../server/db/liveProjectionSchema');
|
||||
const liveProjection = require('../server/services/liveProjectionService');
|
||||
const frontendProjection = require('../public/js/services/liveChannelProjection.js');
|
||||
const grouping = require('../public/js/services/channelVariantGrouping.js');
|
||||
const { channelListWarning } = require('../public/js/services/layaStatusPresentation.js');
|
||||
|
||||
function createFunctionalDb() {
|
||||
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,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(user_id, source_id, item_id, item_type)
|
||||
);
|
||||
`);
|
||||
ensureColumns(db, 'playlist_items', LIVE_PROJECTION_COLUMNS);
|
||||
db.exec(LIVE_PROJECTION_SCHEMA_SQL);
|
||||
return db;
|
||||
}
|
||||
|
||||
function seedCategory(db, categoryId, name, extra = {}) {
|
||||
db.prepare(`
|
||||
INSERT INTO categories (id, source_id, category_id, type, name, is_hidden, data)
|
||||
VALUES (?, 1, ?, 'live', ?, ?, '{}')
|
||||
`).run(`1:${categoryId}`, categoryId, name, extra.isHidden ? 1 : 0);
|
||||
}
|
||||
|
||||
function seedChannel(db, itemId, name, categoryId, extra = {}) {
|
||||
db.prepare(`
|
||||
INSERT INTO playlist_items
|
||||
(id, source_id, item_id, type, name, category_id, stream_icon, stream_url, is_hidden, data)
|
||||
VALUES (?, 1, ?, 'live', ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
`1:${itemId}`,
|
||||
itemId,
|
||||
name,
|
||||
categoryId,
|
||||
`${itemId}.png`,
|
||||
`https://streams.test/${itemId}.m3u8`,
|
||||
extra.isHidden ? 1 : 0,
|
||||
JSON.stringify({
|
||||
tvgId: extra.tvgId || null,
|
||||
groupTitle: extra.groupTitle || undefined
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
test('functional live detection chain keeps categories and language selection correct after Laya fallback', async () => {
|
||||
const db = createFunctionalDb();
|
||||
seedCategory(db, 'sports-fr', 'Sports France');
|
||||
seedCategory(db, 'sports-uk', 'UK Sports');
|
||||
seedCategory(db, 'events', 'Sports Events');
|
||||
seedChannel(db, 'fr-sd', '|FR| Sports One SD', 'sports-fr', { tvgId: 'sports.one.fr' });
|
||||
seedChannel(db, 'fr-4k-hidden', '|FR| Sports One 4K', 'sports-fr', { isHidden: true, tvgId: 'sports.one.fr.4k' });
|
||||
seedChannel(db, 'en-4k', '|UK| Sports One 4K', 'sports-uk', { tvgId: 'sports.one.uk' });
|
||||
seedChannel(db, 'bein-main', 'beIN Sports 1 HD', 'events');
|
||||
seedChannel(db, 'bein-event', 'Partner beIN Sports 1 Event', 'events');
|
||||
db.prepare(`
|
||||
INSERT INTO favorites (user_id, source_id, item_id, item_type)
|
||||
VALUES (7, 1, 'fr-sd', 'channel')
|
||||
`).run();
|
||||
|
||||
let layaRequests = 0;
|
||||
const state = await liveProjection.rebuildWithSignals(db, {
|
||||
mode: 'hybrid',
|
||||
layaClient: {
|
||||
identityHintBatch: async () => {
|
||||
layaRequests += 1;
|
||||
throw new Error('simulated local AI failure');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(layaRequests, 1);
|
||||
assert.equal(state.status, 'success');
|
||||
assert.equal(state.error, null);
|
||||
|
||||
const rawRows = db.prepare(`
|
||||
SELECT item_id, logical_channel_id, grouping_fingerprint
|
||||
FROM playlist_items
|
||||
WHERE item_id IN ('fr-sd', 'fr-4k-hidden', 'en-4k')
|
||||
ORDER BY item_id
|
||||
`).all();
|
||||
assert.deepEqual(rawRows.map(row => row.item_id), ['en-4k', 'fr-4k-hidden', 'fr-sd']);
|
||||
assert.equal(new Set(rawRows.map(row => row.logical_channel_id)).size, 1);
|
||||
assert.deepEqual([...new Set(rawRows.map(row => row.grouping_fingerprint))], ['sports one']);
|
||||
|
||||
const apiChannels = liveProjection.getChannels({ includeHidden: true, userId: 7 }, db);
|
||||
const sportsOne = apiChannels.find(channel => channel.fingerprint === 'sports one');
|
||||
assert.ok(sportsOne);
|
||||
assert.deepEqual(
|
||||
sportsOne.variants
|
||||
.map(variant => [variant.id, variant.categoryName, variant.isHidden, variant.isFavorite])
|
||||
.sort((left, right) => left[0].localeCompare(right[0])),
|
||||
[
|
||||
['en-4k', 'UK Sports', false, false],
|
||||
['fr-4k-hidden', 'Sports France', true, false],
|
||||
['fr-sd', 'Sports France', false, true]
|
||||
]
|
||||
);
|
||||
|
||||
const mapped = frontendProjection.mapResponse({
|
||||
projection: { status: 'success', stale: false },
|
||||
channels: apiChannels
|
||||
}, [{ id: 1, type: 'm3u', name: 'Fixture Provider France' }]);
|
||||
const warning = channelListWarning('hybrid', { modelPresent: true, runtimeAvailable: true }, true);
|
||||
const logical = grouping.groupChannels(mapped.channels, {
|
||||
languageMode: warning ? 'algo' : 'hybrid',
|
||||
preferredLanguage: 'fr',
|
||||
isFavorite: variant => mapped.favorites.has(`${variant.sourceId}:${variant.id}`),
|
||||
isHidden: variant => mapped.hiddenItems.has(`channel:${variant.sourceId}:${variant.streamId || variant.id}`)
|
||||
});
|
||||
const frontendSportsOne = logical.find(channel => channel.fingerprint === 'sports one');
|
||||
|
||||
assert.equal(
|
||||
warning,
|
||||
'Local AI language detection failed. Channels were grouped with Standard matching and remain available.'
|
||||
);
|
||||
assert.ok(frontendSportsOne);
|
||||
assert.equal(frontendSportsOne.variants.length, 3);
|
||||
assert.deepEqual(frontendSportsOne.availableLanguages.map(language => language.code).sort(), ['en', 'fr']);
|
||||
assert.equal(frontendSportsOne.preferredVariant.id, 'fr-sd');
|
||||
assert.equal(frontendSportsOne.preferredVariant.groupTitle, 'Sports France');
|
||||
assert.equal(frontendSportsOne.isFavorite, true);
|
||||
assert.equal(frontendSportsOne.isHidden, false);
|
||||
assert.equal(grouping.orderFlatChannels(logical).length, logical.length);
|
||||
});
|
||||
Reference in New Issue
Block a user