'use strict'; /** * Tests: ИИ-репетитор тренажёра (разбор ошибки / подсказка). * - explain (LLM застаблен): mistake/hint дают текст; grounding (ответ в промпте); off → not ok. * - cleanText: снимает markdown-обёртки и экранирует HTML. * - endpoint /explain: auth-only (ученику доступен); без провайдера → 503; без задачи → 400. */ const { describe, it, before, after } = require('node:test'); const assert = require('node:assert/strict'); const { app, inject, getToken, cleanup } = require('./setup'); const svc = require('../src/services/practiceExplainService'); app.use('/api/practice', require('../src/routes/practice')); after(() => cleanup()); const PROBLEM = { display: '3x + 7 = 22', answer: 'x = 5', solution: [{ note: 'Переносим 7', tex: '3*x = 15' }, { note: 'Делим на 3', tex: 'x = 5' }] }; describe('practiceExplainService.explain (LLM застаблен)', () => { it('mistake: возвращает текст и передаёт модели правильный ответ (grounding)', async () => { let seen = null; const ask = async (messages) => { seen = messages; return { text: 'Ты не поделил обе части на 3.' }; }; const r = await svc.explain({ problem: PROBLEM, studentAnswer: '15', mode: 'mistake', ask }); assert.equal(r.ok, true); assert.equal(r.mode, 'mistake'); assert.ok(r.text.indexOf('поделил') !== -1); const userMsg = seen.map(m => m.content).join('\n'); assert.ok(userMsg.indexOf('x = 5') !== -1, 'правильный ответ передан модели'); assert.ok(userMsg.indexOf('15') !== -1, 'ответ ученика передан модели'); }); it('hint: не просит раскрывать ответ ученика (нет строки «Ответ ученика»)', async () => { let seen = null; const ask = async (messages) => { seen = messages; return { text: 'Что нужно сделать со свободным членом 7?' }; }; const r = await svc.explain({ problem: PROBLEM, mode: 'hint', ask }); assert.equal(r.ok, true); assert.equal(r.mode, 'hint'); const sys = seen[0].content; assert.ok(sys.indexOf('НЕ называй итоговый ответ') !== -1, 'hint-режим запрещает раскрывать ответ'); }); it('off-провайдер → not ok', async () => { const r = await svc.explain({ problem: PROBLEM, mode: 'hint', ask: async () => ({ text: null, error: 'off' }) }); assert.equal(r.ok, false); assert.equal(r.error, 'off'); }); it('cleanText снимает markdown и экранирует HTML', () => { const out = svc.cleanText('```\nОшибка в знаке тут\n```'); assert.ok(out.indexOf('```') === -1, 'нет кодовых блоков'); assert.ok(out.indexOf('') === -1 && out.indexOf('<b>') !== -1, 'HTML экранирован'); }); }); describe('/api/practice/explain endpoint', () => { let student; before(async () => { student = (await getToken('student')).token; }); it('доступен ученику; без провайдера → 503', async () => { const res = await inject('POST', '/api/practice/explain', { display: '3x + 7 = 22', answer: 'x = 5', steps: PROBLEM.solution, studentAnswer: '15', mode: 'mistake' }, student); assert.equal(res.status, 503, `got ${res.status}`); assert.equal(res.body.error, 'off'); }); it('без текста задачи → 400', async () => { const res = await inject('POST', '/api/practice/explain', { display: '', mode: 'hint' }, student); assert.equal(res.status, 400, `got ${res.status}`); }); it('без авторизации → 401', async () => { const res = await inject('POST', '/api/practice/explain', { display: '3x = 6', mode: 'hint' }, null); assert.equal(res.status, 401, `got ${res.status}`); }); });