Files
NewTube/mcp/tools.test.mjs
bruno 148a00e60f
CI / build-and-test (push) Successful in 14m6s
feat(couverture): 100pc API/MCP/tests/docs - volet 2
- tests: server/tests/api_coverage2.test.mjs (23 cas offline-safe) :
  oauth (status/url/connections/preview), channels (validations),
  subscriptions batch + groupes CRUD, transcripts history, likes/history
  DELETE, playlists public/view/reorder, telemetry summary, downloads
  401/403/400, ai 503, twitch-token, config.js, rumble 400
- mcp v1.1.0 : 4 outils authentifies (list_playlists/likes/
  subscriptions/watch_history, NEWTUBE_TOKEN) + tests stub
- openapi.json v1.1 : 15 chemins documentes (sante, recherche,
  details, trending, transcript, auth, prefs, likes, subs, downloads,
  oauth, playlists) ; guide maj (11 outils)
2026-09-27 18:51:50 -04:00

129 lines
6.5 KiB
JavaScript

// Tests des 7 outils MCP contre un stub HTTP (aucune API réelle, aucun réseau).
// Stub -> NEWTUBE_API_URL, puis pilotage stdio JSON-RPC (tools/call).
// Run: npm run test:mcp (inclus via --test mcp/*.test.mjs si appelé en glob,
// sinon exécution directe node --test mcp/tools.test.mjs)
import { describe, it, before, after } from 'node:test';
import assert from 'node:assert/strict';
import http from 'node:http';
import { spawn } from 'node:child_process';
const LINES = [
{ t: 1, dur: 2, text: 'Bonjour' },
{ t: 65, dur: 2.5, text: 'deuxième ligne' },
{ t: 120, dur: 2, text: `remplissage ${'x'.repeat(900)}` },
];
const stub = http.createServer((req, res) => {
const u = new URL(req.url, 'http://x');
const json = (code, obj) => { res.writeHead(code, { 'content-type': 'application/json' }); res.end(JSON.stringify(obj)); };
if (u.pathname === '/api/healthz') return json(200, { status: 'ok', youtube: { mode: 'test' } });
if (u.pathname === '/api/search') return json(200, { q: 'q', providers: ['yt'], groups: { yt: [{ id: 'v1', title: 'T' }] }, page: 1, pageSize: 10 });
if (u.pathname === '/api/search/suggest') return json(200, { q: 'q', groups: { yt: ['abc'] } });
if (u.pathname === '/api/details/youtube/v1') return json(200, { videoId: 'v1', title: 'Demo', uploaderName: 'U', duration: 60, views: 10 });
if (u.pathname === '/api/trending') return json(200, { provider: 'yt', items: [{ id: 't1' }] });
if (u.pathname === '/api/transcript/youtube/v1') return json(200, { lang: 'fr', available: true, languages: ['fr', 'en'], lines: LINES });
if (u.pathname === '/api/transcript/youtube/none') return json(200, { lang: null, available: false, languages: [], lines: [], reason: 'no_subtitles' });
if (u.pathname === '/api/playlists') return json(200, [{ id: 'p1', title: 'Ma playlist' }]);
if (u.pathname === '/api/user/likes') return json(200, [{ provider: 'youtube', videoId: 'v1' }]);
if (u.pathname === '/api/subscriptions') return json(200, { items: [{ subscriptionId: 1 }], ttl: 1 });
if (u.pathname === '/api/user/history/watch') return json(200, [{ provider: 'youtube', videoId: 'v1' }]);
return json(404, { error: 'not_found' });
});
await new Promise((r) => stub.listen(0, '127.0.0.1', r));
const STUB_URL = `http://127.0.0.1:${stub.address().port}/api`;
function startMcp() {
return spawn(process.execPath, ['./mcp/server.mjs'], { env: { ...process.env, NEWTUBE_API_URL: STUB_URL } });
}
function rpc(proc, id, method, params = {}) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error(`timeout ${method}`)), 15000);
const onData = (buf) => {
for (const l of String(buf).split('\n')) {
if (!l.trim()) continue;
try {
const m = JSON.parse(l);
if (m.id === id) { clearTimeout(timer); proc.stdout.off('data', onData); resolve(m); return; }
} catch {}
}
};
proc.stdout.on('data', onData);
proc.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n');
});
}
async function call(proc, id, name, args) {
const m = await rpc(proc, id, 'tools/call', { name, arguments: args });
assert.ok(!m.error, `RPC error: ${JSON.stringify(m.error)}`);
assert.equal(m.result.isError, undefined, `tool ${name} isError: ${m.result.content?.[0]?.text?.slice(0, 300)}`);
return m.result.content[0].text;
}
let proc;
before(async () => {
proc = startMcp();
await rpc(proc, 1, 'initialize');
});
after(() => { try { proc.kill(); } catch {} stub.close(); });
describe('MCP tools (stub API)', () => {
it('search_videos / suggest_queries / get_video_details / get_trending / health_check', async () => {
const t1 = await call(proc, 11, 'search_videos', { q: 'test' });
assert.match(t1, /r.sultats|résultats|1 r/);
const t2 = await call(proc, 12, 'suggest_queries', { q: 'ab' });
assert.match(t2, /Suggestions/);
const t3 = await call(proc, 13, 'get_video_details', { provider: 'youtube', videoId: 'v1' });
assert.match(t3, /Demo/);
const t4 = await call(proc, 14, 'get_trending', { provider: 'yt', limit: 5 });
assert.match(t4, /tendances/);
const t5 = await call(proc, 15, 'health_check', {});
assert.match(t5, /test/);
});
it('get_transcript JSON brut', async () => {
const t = await call(proc, 21, 'get_transcript', { provider: 'youtube', videoId: 'v1', lang: 'fr' });
assert.match(t, /3 lignes/);
assert.match(t, /Bonjour/);
});
it('get_transcript indisponible -> message clair, pas d erreur', async () => {
const t = await call(proc, 22, 'get_transcript', { provider: 'youtube', videoId: 'none' });
assert.match(t, /indisponible/);
assert.match(t, /no_subtitles/);
});
it('get_transcript_text horodaté + srt + troncature', async () => {
const txt = await call(proc, 23, 'get_transcript_text', { provider: 'youtube', videoId: 'v1', lang: 'fr' });
assert.match(txt, /\[00:00:01\] Bonjour/);
assert.match(txt, /\[00:01:05\]/);
const srt = await call(proc, 24, 'get_transcript_text', { provider: 'youtube', videoId: 'v1', format: 'srt' });
assert.match(srt, /-->/);
const cut = await call(proc, 25, 'get_transcript_text', { provider: 'youtube', videoId: 'v1', maxChars: 500 });
assert.match(cut, /tronqu/);
});
it('outil inconnu -> erreur JSON-RPC -32602', async () => {
const m = await rpc(proc, 99, 'tools/call', { name: 'nope', arguments: {} });
assert.ok(m.error);
assert.equal(m.error.code, -32602);
});
it('outils authentifiés sans token -> isError NEWTUBE_TOKEN', async () => {
const m = await rpc(proc, 100, 'tools/call', { name: 'list_playlists', arguments: {} });
assert.equal(m.result.isError, true);
assert.match(m.result.content[0].text, /NEWTUBE_TOKEN/);
});
it('outils authentifiés avec token (stub)', async () => {
const authed = spawn(process.execPath, ['./mcp/server.mjs'], { env: { ...process.env, NEWTUBE_API_URL: STUB_URL, NEWTUBE_TOKEN: 'dummy-jwt' } });
try {
await rpc(authed, 1, 'initialize');
for (const [id, name, args, re] of [
[201, 'list_playlists', {}, /playlist/],
[202, 'list_likes', {}, /likée/],
[203, 'list_subscriptions', {}, /abonnement/],
[204, 'list_watch_history', {}, /historique/],
]) {
const m = await rpc(authed, id, 'tools/call', { name, arguments: args });
assert.equal(m.result.isError, undefined, `${name}: ${m.result.content?.[0]?.text?.slice(0, 200)}`);
assert.match(m.result.content[0].text, re);
}
} finally {
authed.kill();
}
});
});