wordeck/packages/wordeck-domain/tests/fsrs.test.ts
Till JS 372832d266
Some checks are pending
CI / validate (push) Waiting to run
refactor(big-bang): cards → wordeck im gesamten Code-Layer
Phase 2 des cards→wordeck Big-Bang-Rebrand:

- 4 package.json: @cards/* → @wordeck/*
- packages/cards-domain/ → packages/wordeck-domain/
- 41+12 Files: from '@cards/domain' → '@wordeck/domain'
- pgSchema('cards') → pgSchema('wordeck') (Drizzle-Schema)
- 17 Files: process.env.CARDS_* → process.env.WORDECK_*
- docker-compose Service-Names: cards-* → wordeck-*
- docker-compose Volume: /Volumes/ManaData/cards → wordeck
- env-vars in compose: CARDS_DB_PASSWORD/_API_VERSION/_DSGVO_SERVICE_KEY etc. → WORDECK_*
- Log-Prefixes + Error-Strings + manifest-id 'cards' → 'wordeck'
- CORS-Origin cardecky.mana.how → wordeck.com
- .env.production.example umbenannt + S3-Key entfernt (kein MinIO mehr)

Type-Check 0 Errors in api+domain+web, 51/51 Domain-Tests grün.

DB-Rename + Container/Volume-Rename auf mana-server folgen in nächstem
Commit nach Verzeichnis-Rename.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 22:39:42 +02:00

100 lines
2.8 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import {
buildScheduler,
fromFsrsCard,
gradeReview,
newReview,
subIndexCount,
toFsrsCard,
} from '../src/fsrs.ts';
describe('newReview', () => {
it('initializes with state=new and reps=0', () => {
const r = newReview({
userId: 'u-1',
cardId: 'c-1',
now: new Date('2026-05-08T10:00:00Z'),
});
expect(r.card_id).toBe('c-1');
expect(r.sub_index).toBe(0);
expect(r.user_id).toBe('u-1');
expect(r.state).toBe('new');
expect(r.reps).toBe(0);
expect(r.lapses).toBe(0);
expect(r.due).toBe('2026-05-08T10:00:00.000Z');
});
it('honours provided sub_index', () => {
const r = newReview({ userId: 'u-1', cardId: 'c-1', subIndex: 2 });
expect(r.sub_index).toBe(2);
});
});
describe('gradeReview', () => {
const fixedNow = new Date('2026-05-08T10:00:00Z');
const reviewedAt = new Date('2026-05-08T10:01:00Z');
const baseReview = newReview({
userId: 'u-1',
cardId: 'c-1',
now: fixedNow,
});
it('Again from new keeps reps=0 increments lapses', () => {
// Disable fuzz for deterministic test outputs
const next = gradeReview(baseReview, 'again', reviewedAt, { enable_fuzz: false });
expect(next.state).not.toBe('new');
expect(next.due).not.toBe(baseReview.due);
expect(next.reps).toBeGreaterThanOrEqual(1);
});
it('Easy from new transitions to a future-dated review', () => {
const next = gradeReview(baseReview, 'easy', reviewedAt, { enable_fuzz: false });
expect(new Date(next.due).getTime()).toBeGreaterThan(reviewedAt.getTime());
});
it('preserves card_id, sub_index, user_id', () => {
const next = gradeReview(baseReview, 'good', reviewedAt, { enable_fuzz: false });
expect(next.card_id).toBe(baseReview.card_id);
expect(next.sub_index).toBe(baseReview.sub_index);
expect(next.user_id).toBe(baseReview.user_id);
});
});
describe('toFsrsCard / fromFsrsCard roundtrip', () => {
it('roundtrips a new review without loss', () => {
const r = newReview({ userId: 'u-1', cardId: 'c-1' });
const fc = toFsrsCard(r);
const back = fromFsrsCard(r, fc);
expect(back.due).toBe(r.due);
expect(back.stability).toBe(r.stability);
expect(back.state).toBe(r.state);
});
});
describe('subIndexCount', () => {
it('basic = 1, basic-reverse = 2', () => {
expect(subIndexCount('basic')).toBe(1);
expect(subIndexCount('basic-reverse')).toBe(2);
});
it('unknown type defaults to 1', () => {
expect(subIndexCount('unknown-future-type')).toBe(1);
});
it('cloze wirft — Caller muss subIndexCountForCloze nutzen', () => {
expect(() => subIndexCount('cloze')).toThrow(/subIndexCountForCloze/);
});
});
describe('buildScheduler', () => {
it('builds with defaults', () => {
const s = buildScheduler();
expect(s).toBeDefined();
});
it('honours per-deck overrides', () => {
const s = buildScheduler({ request_retention: 0.85, enable_fuzz: false });
expect(s).toBeDefined();
});
});