mr-magic-mcp-server 0.3.5 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/index.js +15 -5
- package/src/tests/run-tests.js +49 -3
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -43,7 +43,9 @@ function rankRecord(record) {
|
|
|
43
43
|
async function tryProviders(track, { syncedOnly = false, providerNames = [] } = {}) {
|
|
44
44
|
const matches = [];
|
|
45
45
|
let bestSynced = null;
|
|
46
|
-
|
|
46
|
+
// Only track candidates that have actual lyric text — metadata-only stubs
|
|
47
|
+
// (e.g. Melon returning a record with plainLyrics: null) never become best.
|
|
48
|
+
let bestWithContent = null;
|
|
47
49
|
const chosenProviders =
|
|
48
50
|
providerNames.length > 0
|
|
49
51
|
? providers.filter((provider) => providerNames.includes(provider.name))
|
|
@@ -56,15 +58,23 @@ async function tryProviders(track, { syncedOnly = false, providerNames = [] } =
|
|
|
56
58
|
const scored = { provider: provider.name, result: candidate, score: rankRecord(candidate) };
|
|
57
59
|
matches.push(scored);
|
|
58
60
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
// Only promote as best when the candidate actually has lyric content.
|
|
62
|
+
if (lyricContentScore(candidate) > 0) {
|
|
63
|
+
if (!bestWithContent || scored.score > bestWithContent.score) {
|
|
64
|
+
bestWithContent = scored;
|
|
65
|
+
}
|
|
61
66
|
}
|
|
62
|
-
if (
|
|
67
|
+
if (
|
|
68
|
+
candidate.synced &&
|
|
69
|
+
lyricContentScore(candidate) > 0 &&
|
|
70
|
+
(!bestSynced || scored.score > bestSynced.score)
|
|
71
|
+
) {
|
|
63
72
|
bestSynced = scored;
|
|
64
73
|
}
|
|
65
74
|
}
|
|
66
75
|
|
|
67
|
-
|
|
76
|
+
// best is null when no provider returned actual lyric content for this track.
|
|
77
|
+
const best = syncedOnly ? (bestSynced ?? null) : (bestSynced ?? bestWithContent ?? null);
|
|
68
78
|
return {
|
|
69
79
|
matches,
|
|
70
80
|
best
|
package/src/tests/run-tests.js
CHANGED
|
@@ -259,6 +259,41 @@ function testAutoPickSyncedWithContentBeatsSyncedEmpty() {
|
|
|
259
259
|
console.log('autoPick: synced+content beats synced+empty even with lower confidence: ok');
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
+
function testEmptyRecordNeverBecomesBest() {
|
|
263
|
+
// Simulate the tryProviders bestWithContent guard directly.
|
|
264
|
+
// When all candidates are metadata-only (no lyric text), bestWithContent must stay null.
|
|
265
|
+
const emptyRecords = [
|
|
266
|
+
{ plainLyrics: null, syncedLyrics: null, synced: false, confidence: 0.5 }, // Melon stub
|
|
267
|
+
{ plainLyrics: '', syncedLyrics: null, synced: false, confidence: 0.3 } // empty string
|
|
268
|
+
];
|
|
269
|
+
let bestWithContent = null;
|
|
270
|
+
for (const candidate of emptyRecords) {
|
|
271
|
+
if (lyricContentScore(candidate) > 0) {
|
|
272
|
+
bestWithContent = candidate;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
assert.ok(
|
|
276
|
+
bestWithContent === null,
|
|
277
|
+
'metadata-only records must not be promoted as best — bestWithContent should remain null'
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
// Contrast: a record with actual text IS selected
|
|
281
|
+
const withLyrics = { plainLyrics: 'line one\nline two', syncedLyrics: null, confidence: 0.1 };
|
|
282
|
+
let bestWithContentFromMixed = null;
|
|
283
|
+
for (const candidate of [...emptyRecords, withLyrics]) {
|
|
284
|
+
if (lyricContentScore(candidate) > 0) {
|
|
285
|
+
bestWithContentFromMixed = candidate;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
assert.ok(
|
|
289
|
+
bestWithContentFromMixed === withLyrics,
|
|
290
|
+
'when a content-bearing record is present, it should be promoted over empty ones'
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
divider();
|
|
294
|
+
console.log('empty records never become best — content guard works: ok');
|
|
295
|
+
}
|
|
296
|
+
|
|
262
297
|
async function testBuildPayloadFromResultReturnsCacheKey() {
|
|
263
298
|
// build a minimal find result with plain lyrics — no network call needed
|
|
264
299
|
const best = {
|
|
@@ -273,7 +308,10 @@ async function testBuildPayloadFromResultReturnsCacheKey() {
|
|
|
273
308
|
const context = buildActionContext({});
|
|
274
309
|
const payload = await buildPayloadFromResult(result, context);
|
|
275
310
|
|
|
276
|
-
assert.ok(
|
|
311
|
+
assert.ok(
|
|
312
|
+
payload.lyricsCacheKey,
|
|
313
|
+
'buildPayloadFromResult should include lyricsCacheKey when best has plain lyrics'
|
|
314
|
+
);
|
|
277
315
|
assert.equal(typeof payload.lyricsCacheKey, 'string', 'lyricsCacheKey should be a string');
|
|
278
316
|
|
|
279
317
|
// Verify the cache was actually populated
|
|
@@ -283,7 +321,11 @@ async function testBuildPayloadFromResultReturnsCacheKey() {
|
|
|
283
321
|
|
|
284
322
|
// Verify key stability — same artist/title always yields the same key
|
|
285
323
|
const expectedKey = catalogCacheKey({ artist: 'Dylan Cotrone', title: 'Cigarette' });
|
|
286
|
-
assert.equal(
|
|
324
|
+
assert.equal(
|
|
325
|
+
payload.lyricsCacheKey,
|
|
326
|
+
expectedKey,
|
|
327
|
+
'lyricsCacheKey should match catalogCacheKey for the track'
|
|
328
|
+
);
|
|
287
329
|
|
|
288
330
|
divider();
|
|
289
331
|
console.log('buildPayloadFromResult returns lyricsCacheKey and populates cache: ok');
|
|
@@ -302,7 +344,10 @@ async function testBuildPayloadFromResultNoCacheKeyWhenNoLyrics() {
|
|
|
302
344
|
const context = buildActionContext({});
|
|
303
345
|
const payload = await buildPayloadFromResult(result, context);
|
|
304
346
|
|
|
305
|
-
assert.ok(
|
|
347
|
+
assert.ok(
|
|
348
|
+
!payload.lyricsCacheKey,
|
|
349
|
+
'buildPayloadFromResult should NOT include lyricsCacheKey when best has no lyrics'
|
|
350
|
+
);
|
|
306
351
|
|
|
307
352
|
divider();
|
|
308
353
|
console.log('buildPayloadFromResult omits lyricsCacheKey when best has no lyrics: ok');
|
|
@@ -320,6 +365,7 @@ async function run() {
|
|
|
320
365
|
testAutoPickPrefersContentOverEmpty();
|
|
321
366
|
testAutoPickRicherContentWins();
|
|
322
367
|
testAutoPickSyncedWithContentBeatsSyncedEmpty();
|
|
368
|
+
testEmptyRecordNeverBecomesBest();
|
|
323
369
|
await testBuildPayloadFromResultReturnsCacheKey();
|
|
324
370
|
await testBuildPayloadFromResultNoCacheKeyWhenNoLyrics();
|
|
325
371
|
const toolNames = mcpToolDefinitions.map((tool) => tool.name);
|