test: cover functional live detection chain with Laya fallback (#18)

This commit is contained in:
blomios
2026-10-05 19:34:42 +02:00
parent 3f44217e42
commit 6dfd2ba093

View 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);
});