'use strict'; /** * Integration tests: интервальное повторение флешкарт (Tier-1 апгрейд). * Covers: learning-steps (новая карта проходит шаги в минутах), выпуск в review * через «Знаю»/«Легко», lapse зрелой карты → relearning, флаг graduated для * клиентского re-queue, и лимит новых карт/день в study-сессии. */ const { describe, it, before, after } = require('node:test'); const assert = require('node:assert/strict'); const { app, db, inject, getToken, cleanup } = require('./setup'); // Маршрут флешкарт setup.js не монтирует (как и custom-sims) — монтируем сами. app.use('/api/flashcards', require('../src/routes/flashcards')); after(() => cleanup()); /* helpers */ async function mkDeck(token, title = 'Колода') { const r = await inject('POST', '/api/flashcards/decks', { title }, token); assert.equal(r.status, 200, `deck create: ${r.status} ${JSON.stringify(r.body)}`); return r.body.id; } async function mkCard(token, deckId, front = 'Q', back = 'A') { const r = await inject('POST', `/api/flashcards/decks/${deckId}/cards`, { front, back }, token); assert.equal(r.status, 200, `card create: ${r.status}`); return r.body.id; } function review(token, cardId, quality) { return inject('POST', `/api/flashcards/cards/${cardId}/review`, { quality }, token); } describe('flashcards SRS — learning steps & limits', () => { let token; before(async () => { token = (await getToken('student')).token; }); it('review validates quality range (0..5)', async () => { const deck = await mkDeck(token); const card = await mkCard(token, deck); assert.equal((await review(token, card, 7)).status, 400); assert.equal((await review(token, card, -1)).status, 400); }); it('new card + «Снова» (q0) → learning, due через 1 минуту, не выпущена', async () => { const deck = await mkDeck(token); const card = await mkCard(token, deck); const r = await review(token, card, 0); assert.equal(r.status, 200); assert.equal(r.body.graduated, false, 'не выпущена'); assert.equal(r.body.due_in_sec, 60, '1 минута'); assert.equal(r.body.next.state, 'learning'); assert.equal(r.body.next.learning_step, 0); }); it('new card + «Знаю» (q4) → второй шаг 10 минут, всё ещё learning', async () => { const deck = await mkDeck(token); const card = await mkCard(token, deck); const r = await review(token, card, 4); assert.equal(r.body.graduated, false); assert.equal(r.body.due_in_sec, 600, '10 минут'); assert.equal(r.body.next.state, 'learning'); assert.equal(r.body.next.learning_step, 1); }); it('two «Знаю» подряд выпускают карту в review (interval 1 день)', async () => { const deck = await mkDeck(token); const card = await mkCard(token, deck); await review(token, card, 4); // шаг 0 → 1 const r = await review(token, card, 4); // шаг 1 → выпуск assert.equal(r.body.graduated, true, 'выпущена в review'); assert.equal(r.body.next.state, 'review'); assert.equal(r.body.interval_days, 1); assert.equal(r.body.due_in_sec, 86400); }); it('«Легко» (q5) на новой карте выпускает сразу (interval 4 дня)', async () => { const deck = await mkDeck(token); const card = await mkCard(token, deck); const r = await review(token, card, 5); assert.equal(r.body.graduated, true); assert.equal(r.body.next.state, 'review'); assert.equal(r.body.interval_days, 4); }); it('зрелая карта + «Снова» → lapse: relearning, lapses=1, due 10 минут', async () => { const deck = await mkDeck(token); const card = await mkCard(token, deck); await review(token, card, 5); // сразу в review (iv=4) const r = await review(token, card, 0); // провал assert.equal(r.body.graduated, false, 'ушла на переучивание'); assert.equal(r.body.next.state, 'relearning'); assert.equal(r.body.due_in_sec, 600, 'relearn-шаг 10 минут'); // lapses фиксируется в БД const row = db.prepare('SELECT lapses FROM flashcard_reviews WHERE card_id=?').get(card); assert.equal(row.lapses, 1); }); it('зрелая карта + «Знаю» растёт по дням (≥ предыдущего интервала)', async () => { const deck = await mkDeck(token); const card = await mkCard(token, deck); await review(token, card, 5); // iv=4, review const r = await review(token, card, 4); // Знаю на зрелой assert.equal(r.body.next.state, 'review'); assert.ok(r.body.interval_days > 4, `интервал вырос: ${r.body.interval_days}`); }); it('study-сессия уважает лимит новых карт/день (new_per_day)', async () => { const deck = await mkDeck(token, 'Большая'); db.prepare('UPDATE flashcard_decks SET new_per_day=? WHERE id=?').run(2, deck); for (let i = 0; i < 5; i++) await mkCard(token, deck, 'Q' + i, 'A' + i); const s1 = await inject('GET', `/api/flashcards/decks/${deck}/study`, null, token); assert.equal(s1.status, 200); assert.equal(s1.body.cards.length, 2, 'не больше лимита новых'); assert.ok(s1.body.cards.every(c => c.state === 'new'), 'все — новые'); // «вводим» обе карты (review) — бюджет на сегодня исчерпан for (const c of s1.body.cards) await review(token, c.id, 4); const s2 = await inject('GET', `/api/flashcards/decks/${deck}/study`, null, token); // введённые карты теперь learning (due через минуты, не сейчас) → сессия пуста assert.equal(s2.body.cards.length, 0, 'бюджет новых исчерпан, learning ещё не due'); }); it('study-сессия возвращает state/learning_step для превью кнопок', async () => { const deck = await mkDeck(token, 'Превью'); const card = await mkCard(token, deck); const s = await inject('GET', `/api/flashcards/decks/${deck}/study`, null, token); const c = s.body.cards.find(x => x.id === card); assert.ok(c, 'карта в сессии'); assert.equal(c.state, 'new'); assert.equal(c.learning_step, 0); assert.equal(c.seen, 0); }); });