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>
This commit is contained in:
Maxim Dolgolyov
2026-06-25 14:30:02 +03:00
parent d003a0e100
commit cd7c75ff08
7 changed files with 185 additions and 7 deletions
+12
View File
@@ -159,4 +159,16 @@ describe('/api/practice/class-stats (аналитика класса)', () => {
const res = await inject('GET', '/api/practice/class-stats', null, teacher.token);
assert.equal(res.status, 400, `got ${res.status}`);
});
it('POST /assign владельцем → уведомляет всех учеников', async () => {
const res = await inject('POST', '/api/practice/assign', { class_id: classId, topic: 'word-linear', title: 'Линейные уравнения' }, teacher.token);
assert.equal(res.status, 200, `got ${res.status}`);
assert.equal(res.body.ok, true);
assert.equal(res.body.notified, 2, 'двое учеников уведомлены');
});
it('POST /assign чужой класс → 403', async () => {
const res = await inject('POST', '/api/practice/assign', { class_id: classId, topic: 'word-linear' }, other.token);
assert.equal(res.status, 403, `got ${res.status}`);
});
});