Fix the regression introduced by the weighted Laya signal (#16): rebuild now merges exact fingerprints in every engine mode after evaluating hard blockers, compares candidates against all variants of a group, falls back to deterministic scoring when Laya is unavailable, and reports transaction errors safely. Non-regression tests cover the Laya-mode rebuild path; verified against a real catalog copy (39,341 streams rebuilt successfully). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
149 lines
5.7 KiB
JavaScript
149 lines
5.7 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const {
|
|
MODE_WEIGHTS,
|
|
hardBlock,
|
|
scoreCandidate
|
|
} = require('../server/services/liveGroupingScoring');
|
|
const {
|
|
collectIdentityHints,
|
|
collectOptionalIdentityHints,
|
|
normalizeChannelName,
|
|
projectCatalog
|
|
} = require('../server/services/liveProjectionService');
|
|
|
|
const channel = (id, name, extra = {}) => ({
|
|
id,
|
|
source_id: 1,
|
|
item_id: id,
|
|
name,
|
|
category_id: 'sports',
|
|
fingerprint: normalizeChannelName(name),
|
|
...extra
|
|
});
|
|
|
|
test('weights match the validated architecture and remain bounded', () => {
|
|
assert.deepEqual(MODE_WEIGHTS.algo, { algorithm: 80, structure: 15, metadata: 5, laya: 0 });
|
|
assert.deepEqual(MODE_WEIGHTS.hybrid, { algorithm: 70, structure: 15, metadata: 5, laya: 10 });
|
|
assert.deepEqual(MODE_WEIGHTS.laya, { algorithm: 55, structure: 15, metadata: 5, laya: 25 });
|
|
});
|
|
|
|
test('exact algorithm match merges without Laya', () => {
|
|
const left = channel('a', 'beIN Sports 1 HD');
|
|
const right = channel('b', 'beIN Sports 1 4K');
|
|
assert.equal(scoreCandidate(left, right, { mode: 'algo' }).matched, true);
|
|
});
|
|
|
|
test('exact algorithm match survives a contradictory Laya opinion', () => {
|
|
const left = channel('a', 'beIN Sports 1 HD');
|
|
const right = channel('b', 'beIN Sports 1 4K');
|
|
const score = scoreCandidate(left, right, {
|
|
mode: 'hybrid',
|
|
layaHint: { sameIdentityProbability: 0, confidence: 1, number: '2' }
|
|
});
|
|
assert.equal(score.matched, true);
|
|
assert.equal(score.signals.laya, 0);
|
|
});
|
|
|
|
test('exact generic fingerprints merge in Laya-only mode without family metadata', () => {
|
|
const left = channel('a', '1000 x Hora TV', { category_id: 'one' });
|
|
const right = channel('b', '|UY| 1000 x Hora TV', { category_id: 'two' });
|
|
const score = scoreCandidate(left, right, { mode: 'laya' });
|
|
assert.equal(score.total, 55);
|
|
assert.equal(score.matched, true);
|
|
const groups = projectCatalog([left, right], { mode: 'laya' });
|
|
assert.equal(groups.length, 1);
|
|
});
|
|
|
|
test('different channel numbers block even when Laya proposes a merge', () => {
|
|
const left = channel('a', 'beIN Sports 1');
|
|
const right = channel('b', 'beIN Sports 2');
|
|
const score = scoreCandidate(left, right, {
|
|
mode: 'laya', layaHint: { sameIdentityProbability: 1, confidence: 1 }
|
|
});
|
|
assert.equal(score.matched, false);
|
|
assert.equal(score.blocker, 'number_mismatch');
|
|
});
|
|
|
|
test('protected max token blocks beIN Sports Max 10 from beIN Sports 1', () => {
|
|
assert.equal(hardBlock(channel('a', 'beIN Sports Max 10'), channel('b', 'beIN Sports 1')), 'protected_token_mismatch');
|
|
assert.equal(projectCatalog([
|
|
channel('a', 'beIN Sports Max 10'), channel('b', 'beIN Sports 1')
|
|
], { mode: 'laya' }).length, 2);
|
|
});
|
|
|
|
test('different strong families are blocked before weighted scoring', () => {
|
|
assert.equal(hardBlock(channel('a', 'beIN Sports 1'), channel('b', 'Sky Sports 1')), 'strong_family_mismatch');
|
|
});
|
|
|
|
test('low-confidence Laya hint contributes zero', () => {
|
|
const score = scoreCandidate(
|
|
channel('a', 'beIN Sports 1 Event'),
|
|
channel('b', 'beIN Sports 1'),
|
|
{ mode: 'hybrid', layaHint: { sameIdentityProbability: 1, confidence: 0.64 } }
|
|
);
|
|
assert.equal(score.signals.laya, 0);
|
|
assert.equal(score.matched, false);
|
|
});
|
|
|
|
test('No Laya mode never calls the identity client', async () => {
|
|
let calls = 0;
|
|
const hints = await collectIdentityHints([
|
|
channel('a', 'beIN Sports 1 Event'), channel('b', 'beIN Sports 1')
|
|
], 'algo', { identityHintBatch: async () => { calls++; return []; } });
|
|
assert.equal(calls, 0);
|
|
assert.equal(hints.size, 0);
|
|
});
|
|
|
|
test('Laya only calls Laya but still respects blockers', async () => {
|
|
let calls = 0;
|
|
const items = [channel('a', 'beIN Sports 1 Event'), channel('b', 'beIN Sports 1')];
|
|
const hints = await collectIdentityHints(items, 'laya', {
|
|
identityHintBatch: async pairs => {
|
|
calls++;
|
|
return pairs.map(pair => ({ id: pair.id, sameIdentityProbability: 1, confidence: 1 }));
|
|
}
|
|
});
|
|
assert.equal(calls, 1);
|
|
assert.equal(projectCatalog(items, { mode: 'laya', identityHints: hints }).length, 1);
|
|
assert.equal(projectCatalog([
|
|
channel('1', 'beIN Sports 1'), channel('2', 'beIN Sports 2')
|
|
], { mode: 'laya', identityHints: hints }).length, 2);
|
|
});
|
|
|
|
test('hybrid mode caps the positive Laya contribution at weight 10', () => {
|
|
const score = scoreCandidate(
|
|
channel('a', 'beIN Sports 1 Event'),
|
|
channel('b', 'beIN Sports 1'),
|
|
{ mode: 'hybrid', layaHint: { sameIdentityProbability: 1, confidence: 5 } }
|
|
);
|
|
assert.equal(score.signals.laya, 1);
|
|
assert.equal(score.total, 72);
|
|
assert.equal(score.matched, true);
|
|
});
|
|
|
|
test('Laya bridge followed by an exact alias stays in one unique fingerprint group', () => {
|
|
const items = [
|
|
channel('a', 'beIN Sports 1 Event'),
|
|
channel('b', 'beIN Sports 1'),
|
|
channel('c', 'beIN Sports 1')
|
|
];
|
|
const hints = new Map([[
|
|
['a', 'b'].sort().join('\0'),
|
|
{ sameIdentityProbability: 1, confidence: 1 }
|
|
]]);
|
|
const groups = projectCatalog(items, { mode: 'hybrid', identityHints: hints });
|
|
assert.equal(groups.length, 1);
|
|
assert.equal(groups[0].fingerprint, 'bein sports 1');
|
|
assert.equal(groups[0].variants.length, 3);
|
|
});
|
|
|
|
test('optional Laya failure degrades to an empty signal set', async () => {
|
|
const hints = await collectOptionalIdentityHints([
|
|
channel('a', 'beIN Sports 1 Event'), channel('b', 'beIN Sports 1')
|
|
], 'hybrid', {
|
|
identityHintBatch: async () => { throw new Error('runtime unavailable at https://private.invalid'); }
|
|
});
|
|
assert.equal(hints.size, 0);
|
|
});
|