Files
Learn_System/backend/tests/practice-gen.test.js
T
Maxim Dolgolyov cd7c75ff08 feat(trainer): P4 — авторинг задач учителем + раздача классу
- POST /api/practice/author: учитель пишет story/lhs/rhs/answer → та же проверка подстановкой (validateAndVerify) → пул; не сходится → 422
- POST /api/practice/assign: выдать тему классу → durable pushNotif каждому ученику (ссылка /trainer); владелец/админ, чужой → 403
- клиент: LS.practiceAuthor/Assign; в теме «Текстовые задачи» учителю кнопки «Своя задача» (модалка-форма) и «Выдать классу» (пикер классов)
- тесты: author (валид→пул, неверный→422, ученик→403), assign (владелец уведомляет, чужой→403) — practice 19/19 + practice-gen 16/16
- смоук страницы 27/27; план P4 → DONE (lean: ручной авторинг + раздача, без полного DSL-конструктора)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 14:30:02 +03:00

143 lines
6.8 KiB
JavaScript

'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: <b>3x + 4 = 19</b>. Найдите 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('<b>') === -1 && v.problem.story.indexOf('&lt;b&gt;') !== -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('POST /author учителем (валидная) → в пул', async () => {
const res = await inject('POST', '/api/practice/author',
{ topic: 'word-linear', story: 'Задача от учителя', lhs: '2*x + 1', rhs: '7', answer: 3 }, teacher);
assert.equal(res.status, 200, `got ${res.status}`);
assert.equal(res.body.ok, true);
assert.equal(res.body.problem.answer, 3);
assert.equal(res.body.problem.kind, 'word');
});
it('POST /author с неверным корнем → 422 (в пул не попадёт)', async () => {
const res = await inject('POST', '/api/practice/author',
{ topic: 'word-linear', story: 'X', lhs: '2*x + 1', rhs: '7', answer: 5 }, teacher);
assert.equal(res.status, 422, `got ${res.status}`);
});
it('POST /author ученику запрещён (403)', async () => {
const res = await inject('POST', '/api/practice/author',
{ topic: 'word-linear', story: 'X', lhs: '2*x + 1', rhs: '7', answer: 3 }, student);
assert.equal(res.status, 403, `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);
});
});