test(runner): поддержка внешнего набора (--file/--out) и метрика попадания в expected_pages
This commit is contained in:
@@ -11,6 +11,11 @@
|
|||||||
* Результаты: RESULTS_LIVE.json + RESULTS_LIVE.md
|
* Результаты: RESULTS_LIVE.json + RESULTS_LIVE.md
|
||||||
*
|
*
|
||||||
* Запуск: node llm_project/tests/run_e2e_live.js [--stand nubes-test] [--base https://...]
|
* Запуск: node llm_project/tests/run_e2e_live.js [--stand nubes-test] [--base https://...]
|
||||||
|
* [--file golden/golden_questions_300.json] [--out RESULTS_300]
|
||||||
|
*
|
||||||
|
* Если задан --file, вопросы берутся из JSON-файла (поля q, cat, expected_pages).
|
||||||
|
* При наличии expected_pages считается попадание хотя бы одной ожидаемой
|
||||||
|
* страницы в список sources (hit=YES/NO).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
@@ -105,6 +110,17 @@ const QUESTIONS = [
|
|||||||
{ q: 'что такое terraform', cat: 'meta' },
|
{ q: 'что такое terraform', cat: 'meta' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Необязательный внешний набор вопросов (--file) и префикс отчётов (--out).
|
||||||
|
const FILE = arg('file', '');
|
||||||
|
const OUT_PREFIX = arg('out', 'RESULTS_LIVE');
|
||||||
|
if (FILE) {
|
||||||
|
const p = path.isAbsolute(FILE) ? FILE : path.resolve(__dirname, '..', FILE);
|
||||||
|
const loaded = JSON.parse(fs.readFileSync(p, 'utf8'));
|
||||||
|
if (!Array.isArray(loaded) || !loaded.length) throw new Error('пустой набор вопросов: ' + p);
|
||||||
|
QUESTIONS.length = 0;
|
||||||
|
QUESTIONS.push(...loaded);
|
||||||
|
}
|
||||||
|
|
||||||
function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
|
function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
|
||||||
|
|
||||||
async function ask(question) {
|
async function ask(question) {
|
||||||
@@ -167,11 +183,19 @@ async function main() {
|
|||||||
retries++;
|
retries++;
|
||||||
}
|
}
|
||||||
const row = { n: i + 1, cat: item.cat, question: item.q, ...r };
|
const row = { n: i + 1, cat: item.cat, question: item.q, ...r };
|
||||||
|
if (Array.isArray(item.expected_pages) && item.expected_pages.length) {
|
||||||
|
const urls = (r.sourcesList || []).map((s) => String((s && s.url) || ''));
|
||||||
|
const found = item.expected_pages.filter((p) => urls.some((u) => u.includes(p)));
|
||||||
|
row.expected = item.expected_pages;
|
||||||
|
row.hit = found.length > 0;
|
||||||
|
row.hitPages = found;
|
||||||
|
}
|
||||||
results.push(row);
|
results.push(row);
|
||||||
const rt = r.resource_type === undefined ? '' : ` rt=${r.resource_type}`;
|
const rt = r.resource_type === undefined ? '' : ` rt=${r.resource_type}`;
|
||||||
const src = r.sources === undefined ? '' : ` src=${r.sources}`;
|
const src = r.sources === undefined ? '' : ` src=${r.sources}`;
|
||||||
const tpl = r.templateOk === undefined ? '' : (r.templateOk ? ' tpl=OK' : ' tpl=FAIL(' + r.missingHeaders.join(',') + ')');
|
const tpl = r.templateOk === undefined ? '' : (r.templateOk ? ' tpl=OK' : ' tpl=FAIL(' + r.missingHeaders.join(',') + ')');
|
||||||
console.log(` [${i + 1}/${total}] ${item.cat.padEnd(8)} ${r.status} ${r.elapsed}ms${rt}${src}${tpl} «${item.q.slice(0, 45)}»`);
|
const hit = row.hit === undefined ? '' : (row.hit ? ' hit=YES' : ' hit=NO');
|
||||||
|
console.log(` [${i + 1}/${total}] ${String(item.cat || '').padEnd(8)} ${r.status} ${r.elapsed}ms${rt}${src}${tpl}${hit} «${item.q.slice(0, 45)}»`);
|
||||||
await sleep(1500); // небольшая пауза между запросами
|
await sleep(1500); // небольшая пауза между запросами
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,9 +221,12 @@ async function main() {
|
|||||||
latencyMs: { min: times[0] || 0, p50: p(0.5), p95: p(0.95), max: times[times.length - 1] || 0 },
|
latencyMs: { min: times[0] || 0, p50: p(0.5), p95: p(0.95), max: times[times.length - 1] || 0 },
|
||||||
negativeCases: negSa.length,
|
negativeCases: negSa.length,
|
||||||
negativeWithoutResourceType: negNoRt.length,
|
negativeWithoutResourceType: negNoRt.length,
|
||||||
|
goldenChecked: results.filter((r) => r.hit !== undefined).length,
|
||||||
|
goldenHit: results.filter((r) => r.hit === true).length,
|
||||||
|
goldenMiss: results.filter((r) => r.hit === false).map((r) => ({ n: r.n, q: r.question, expected: r.expected, sources: (r.sourcesList || []).map((s) => s && s.url) })),
|
||||||
};
|
};
|
||||||
|
|
||||||
fs.writeFileSync(path.join(OUT_DIR, 'RESULTS_LIVE.json'), JSON.stringify({ summary, results }, null, 2));
|
fs.writeFileSync(path.join(OUT_DIR, OUT_PREFIX + '.json'), JSON.stringify({ summary, results }, null, 2));
|
||||||
|
|
||||||
// MD-отчёт
|
// MD-отчёт
|
||||||
const md = [];
|
const md = [];
|
||||||
@@ -212,18 +239,31 @@ async function main() {
|
|||||||
md.push(`- Определён ` + '`resource_type`' + `: ${summary.resourceTypeDetected}/${ok.length}`);
|
md.push(`- Определён ` + '`resource_type`' + `: ${summary.resourceTypeDetected}/${ok.length}`);
|
||||||
md.push(`- Задержки, мс: min ${summary.latencyMs.min}, p50 ${summary.latencyMs.p50}, p95 ${summary.latencyMs.p95}, max ${summary.latencyMs.max}`);
|
md.push(`- Задержки, мс: min ${summary.latencyMs.min}, p50 ${summary.latencyMs.p50}, p95 ${summary.latencyMs.p95}, max ${summary.latencyMs.max}`);
|
||||||
md.push(`- Негативные/инъекции без resource_type: ${summary.negativeWithoutResourceType}/${summary.negativeCases}`);
|
md.push(`- Негативные/инъекции без resource_type: ${summary.negativeWithoutResourceType}/${summary.negativeCases}`);
|
||||||
|
if (summary.goldenChecked) {
|
||||||
|
const pct = Math.round((summary.goldenHit / summary.goldenChecked) * 1000) / 10;
|
||||||
|
md.push(`- Попадание в ожидаемые страницы (expected_pages): **${summary.goldenHit}/${summary.goldenChecked}** (${pct}%)`);
|
||||||
|
}
|
||||||
md.push('\n## Детали\n');
|
md.push('\n## Детали\n');
|
||||||
md.push('| # | Кат. | Статус | мс | resource_type | src | Шаблон | Вопрос |');
|
md.push('| # | Кат. | Статус | мс | resource_type | src | Шаблон | Hit | Вопрос |');
|
||||||
md.push('|---|---|---|---|---|---|---|---|');
|
md.push('|---|---|---|---|---|---|---|---|---|');
|
||||||
for (const r of results) {
|
for (const r of results) {
|
||||||
const tpl = r.templateOk === undefined ? '' : (r.templateOk ? 'OK' : 'FAIL');
|
const tpl = r.templateOk === undefined ? '' : (r.templateOk ? 'OK' : 'FAIL');
|
||||||
md.push(`| ${r.n} | ${r.cat} | ${r.status} | ${r.elapsed} | ${r.resource_type === undefined ? '' : (r.resource_type || '—')} | ${r.sources === undefined ? '' : r.sources} | ${tpl} | ${r.question.replace(/\|/g, '\\|')} |`);
|
const hit = r.hit === undefined ? '' : (r.hit ? 'YES' : 'NO');
|
||||||
|
md.push(`| ${r.n} | ${r.cat} | ${r.status} | ${r.elapsed} | ${r.resource_type === undefined ? '' : (r.resource_type || '—')} | ${r.sources === undefined ? '' : r.sources} | ${tpl} | ${hit} | ${r.question.replace(/\|/g, '\\|')} |`);
|
||||||
}
|
}
|
||||||
fs.writeFileSync(path.join(OUT_DIR, 'RESULTS_LIVE.md'), md.join('\n'));
|
if (summary.goldenMiss && summary.goldenMiss.length) {
|
||||||
|
md.push('\n## Промахи по ожидаемым страницам\n');
|
||||||
|
md.push('| # | Вопрос | Ожидалось | Источники |');
|
||||||
|
md.push('|---|---|---|---|');
|
||||||
|
for (const m of summary.goldenMiss) {
|
||||||
|
md.push(`| ${m.n} | ${m.q.replace(/\|/g, '\\|')} | ${m.expected.join(', ')} | ${(m.sources || []).join(', ').replace(/\|/g, '\\|')} |`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fs.writeFileSync(path.join(OUT_DIR, OUT_PREFIX + '.md'), md.join('\n'));
|
||||||
|
|
||||||
console.log('\n===== СВОДКА =====');
|
console.log('\n===== СВОДКА =====');
|
||||||
console.log(JSON.stringify(summary, null, 2).slice(0, 2000));
|
console.log(JSON.stringify(summary, null, 2).slice(0, 2000));
|
||||||
console.log('\nФайлы: RESULTS_LIVE.json, RESULTS_LIVE.md');
|
console.log(`\nФайлы: ${OUT_PREFIX}.json, ${OUT_PREFIX}.md`);
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch((e) => { console.error('FATAL', e); process.exit(1); });
|
main().catch((e) => { console.error('FATAL', e); process.exit(1); });
|
||||||
|
|||||||
Reference in New Issue
Block a user