'use strict'; /** * Tests: текстовые задачи тренажёра (Уровень 1) — генерация + проверка + пул. * - validateAndVerify: корректную принимает, неверный корень/мусор отвергает, текст экранирует. * - generate (LLM застаблен): валидная с 1 попытки; ретраи; провал → unverified; провайдер off. * - endpoints: /generate только учитель/админ (403 ученику; 503 без провайдера); /pool отдаёт пул. */ const { describe, it, before, after } = require('node:test'); const assert = require('node:assert/strict'); const { app, db, inject, getToken, cleanup } = require('./setup'); const gen = require('../src/services/practiceGenService'); app.use('/api/practice', require('../src/routes/practice')); after(() => cleanup()); const GOOD = { story: 'Задумали число x: 3x + 4 = 19. Найдите x.', lhs: '3*x + 4', rhs: '19', answer: 5, answerVar: 'x', solution: [{ note: 'Перенесём 4', tex: '3*x = 15' }] }; describe('practiceGenService.validateAndVerify', () => { it('принимает корректную задачу и экранирует текст', () => { const v = gen.validateAndVerify(GOOD); assert.equal(v.ok, true, v.reason); assert.equal(v.problem.answer, 5); assert.ok(v.problem.story.indexOf('') === -1 && v.problem.story.indexOf('<b>') !== -1, 'story escaped'); assert.equal(v.problem.solution[0].tex, '3*x = 15'); }); it('отвергает неверный корень (подстановка не сходится)', () => { const v = gen.validateAndVerify(Object.assign({}, GOOD, { answer: 6 })); assert.equal(v.ok, false); assert.ok(/verify-failed/.test(v.reason), v.reason); }); it('отвергает невалидное выражение', () => { const v = gen.validateAndVerify(Object.assign({}, GOOD, { lhs: '3x +' })); assert.equal(v.ok, false); assert.equal(v.reason, 'expr-parse'); }); it('отвергает без условия', () => { const v = gen.validateAndVerify(Object.assign({}, GOOD, { story: '' })); assert.equal(v.ok, false); assert.equal(v.reason, 'no-story'); }); it('сбрасывает мусорный tex шага в пустую строку', () => { const v = gen.validateAndVerify(Object.assign({}, GOOD, { solution: [{ note: 'ok', tex: 'не выражение!!!' }] })); assert.equal(v.ok, true); assert.equal(v.problem.solution[0].tex, ''); }); }); describe('practiceGenService.generate (LLM застаблен)', () => { const askValid = async () => ({ text: '```json\n' + JSON.stringify(GOOD) + '\n```' }); const askWrong = async () => ({ text: JSON.stringify(Object.assign({}, GOOD, { answer: 99 })) }); const askOff = async () => ({ text: null, error: 'off' }); it('валидная задача с первой попытки', async () => { const r = await gen.generate('word-linear', { ask: askValid, maxRetries: 3 }); assert.equal(r.ok, true); assert.equal(r.attempts, 1); assert.equal(r.problem.answer, 5); }); it('ретраит и берёт валидную со второй попытки', async () => { let n = 0; const ask = async () => { n++; return n === 1 ? { text: 'мусор без json' } : { text: JSON.stringify(GOOD) }; }; const r = await gen.generate('word-linear', { ask, maxRetries: 3 }); assert.equal(r.ok, true); assert.equal(r.attempts, 2); }); it('неверный корень N раз → unverified (в пул не попадёт)', async () => { const r = await gen.generate('word-linear', { ask: askWrong, maxRetries: 3 }); assert.equal(r.ok, false); assert.equal(r.error, 'unverified'); assert.equal(r.attempts, 3); }); it('нет провайдера → off', async () => { const r = await gen.generate('word-linear', { ask: askOff, maxRetries: 3 }); assert.equal(r.ok, false); assert.equal(r.error, 'off'); }); }); describe('/api/practice pool endpoints', () => { let teacher, student; before(async () => { teacher = (await getToken('teacher')).token; student = (await getToken('student')).token; }); it('POST /generate запрещён ученику (403)', async () => { const res = await inject('POST', '/api/practice/generate', { topic: 'word-linear' }, student); assert.equal(res.status, 403, `got ${res.status}`); }); it('POST /generate учителю без провайдера → 503 (off)', async () => { const res = await inject('POST', '/api/practice/generate', { topic: 'word-linear' }, teacher); assert.equal(res.status, 503, `got ${res.status}`); assert.equal(res.body.error, 'off'); }); it('POST /generate неизвестная тема → 400', async () => { const res = await inject('POST', '/api/practice/generate', { topic: 'nope' }, teacher); assert.equal(res.status, 400, `got ${res.status}`); }); it('GET /pool отдаёт одобренные задачи', async () => { db.prepare(`INSERT INTO practice_problems (topic, skill, difficulty, story, lhs, rhs, answer_var, answer, solution_json, status) VALUES ('word-linear','word-linear',1,'Условие','3*x + 4','19','x',5,'[]','approved')`).run(); const res = await inject('GET', '/api/practice/pool?skill=word-linear', null, student); assert.equal(res.status, 200, `got ${res.status}`); assert.ok(Array.isArray(res.body.problems)); const p = res.body.problems.find(x => x.skill === 'word-linear'); assert.ok(p, 'pool problem present'); assert.equal(p.kind, 'word'); assert.equal(p.lhsExpr, '3*x + 4'); assert.equal(p.answer, 5); }); });