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'); function createDb() { 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 seedChannel(db, id, name, extra = {}) { 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, ?, 'live', ?, 'general', '', ?, '{}', ?, ?, ?, ?) `).run( `1:${id}`, id, name, `https://streams.test/${id}.m3u8`, extra.languageCode || null, extra.languageConfidence || null, extra.languageDetector || null, extra.languageInputHash || null ); } test('background projection rebuild keeps a persisted stream language instead of redetecting it', () => { const db = createDb(); seedChannel(db, 'persisted', 'World News', { languageCode: 'fr', languageConfidence: 0.99, languageDetector: 'laya', languageInputHash: 'stable-language-input' }); try { liveProjection.rebuild(db, { mode: 'algo' }); const row = db.prepare(` SELECT language_code, language_detector, language_confidence FROM playlist_items WHERE id = '1:persisted' `).get(); assert.equal(row.language_code, 'fr'); assert.equal(row.language_detector, 'laya'); assert.equal(Number(row.language_confidence), 0.99); } finally { db.close(); } }); test('Laya analysis for one newly detected stream does not send old catalog pairs', async () => { const db = createDb(); for (let index = 0; index < 8; index++) { seedChannel(db, `old-${index}`, `beIN Sports 1 legacy ${index}`, { languageCode: 'fr', languageConfidence: 0.95, languageDetector: 'laya', languageInputHash: `old-${index}-hash` }); } seedChannel(db, 'new-stream', 'beIN Sports 1 new feed'); const oldCatalogPairs = []; const layaClient = { identityHintBatch: async pairs => { for (const pair of pairs) { if (!pair.left.name.includes('new feed') && !pair.right.name.includes('new feed')) { oldCatalogPairs.push(pair); } } return pairs.map(pair => ({ id: pair.id, sameIdentityProbability: 0.9, confidence: 0.9, family: 'bein sports', number: '1' })); } }; try { await liveProjection.rebuildWithSignals(db, { mode: 'laya', layaClient }); assert.equal(oldCatalogPairs.length, 0, `unexpected old-catalog Laya pairs=${oldCatalogPairs.length}`); } finally { db.close(); } }); test('analyzeAll sends persisted catalog pairs to Laya', async () => { const db = createDb(); seedChannel(db, 'persisted-a', 'beIN Sports 1 east', { languageCode: 'fr', languageConfidence: 0.95, languageDetector: 'laya', languageInputHash: 'persisted-a-hash' }); seedChannel(db, 'persisted-b', 'beIN Sports 1 west', { languageCode: 'fr', languageConfidence: 0.95, languageDetector: 'laya', languageInputHash: 'persisted-b-hash' }); let layaPairs = 0; const layaClient = { identityHintBatch: async pairs => { layaPairs += pairs.length; return pairs.map(pair => ({ id: pair.id, sameIdentityProbability: 0.9, confidence: 0.9, family: 'bein sports', number: '1' })); } }; try { await liveProjection.rebuildWithSignals(db, { mode: 'laya', layaClient, analyzeAll: true }); assert.ok(layaPairs > 0); } finally { db.close(); } });