import { describe, expect, it } from 'vitest'; import { validateSchedule } from '../curriculumService'; const week = (n, theme, topicIds, duration = 30) => ({ week_number: n, theme, topic_ids: topicIds, estimated_duration: duration, week_rationale: 'r', }); const makeTopic = (id, theme) => ({ id, theme, type: 'concept', learning_relevance: 'include', }); const buildScheduleFromTopics = (topics) => { const ids = topics.map(t => t.id); return Array.from({ length: 26 }, (_, i) => { const chunk = ids.slice(i * 2, i * 2 + 2); return week(i + 1, topics[i * 2]?.theme || 'General', chunk.length ? chunk : [ids[0]]); }); }; describe('validateSchedule', () => { it('does not warn about missing themes when merging was required (themes_kb > 26)', () => { const topics = []; for (let i = 0; i < 60; i++) topics.push(makeTopic(`t${i}`, `Theme ${i % 30}`)); const schedule = buildScheduleFromTopics(topics); const result = validateSchedule(schedule, topics); expect(result.valid).toBe(true); const themeWarning = result.warnings.find(w => w.includes('not scheduled')); expect(themeWarning).toBeUndefined(); }); it('warns about missing themes only when merging was not required', () => { const topics = []; for (let i = 0; i < 10; i++) topics.push(makeTopic(`t${i}`, `Theme ${i}`)); const schedule = Array.from({ length: 26 }, (_, i) => week(i + 1, 'Theme 0', ['t0']) ); const result = validateSchedule(schedule, topics); const themeWarning = result.warnings.find(w => w.includes('not scheduled')); expect(themeWarning).toBeDefined(); expect(themeWarning).toMatch(/9 theme/); }); it('warns when learning topics are absent from every week (real coverage gap)', () => { const topics = [ makeTopic('t1', 'A'), makeTopic('t2', 'A'), makeTopic('t3', 'B'), ]; const schedule = Array.from({ length: 26 }, (_, i) => week(i + 1, 'A', ['t1']) ); const result = validateSchedule(schedule, topics); const coverageWarning = result.warnings.find(w => w.includes('not covered')); expect(coverageWarning).toBeDefined(); expect(coverageWarning).toMatch(/2 learning topic/); }); it('ignores topics with type=fact or learning_relevance=exclude in coverage', () => { const topics = [ makeTopic('t1', 'A'), { ...makeTopic('t2', 'A'), type: 'fact' }, { ...makeTopic('t3', 'B'), learning_relevance: 'exclude' }, ]; const schedule = Array.from({ length: 26 }, (_, i) => week(i + 1, 'A', ['t1']) ); const result = validateSchedule(schedule, topics); expect(result.warnings.find(w => w.includes('not covered'))).toBeUndefined(); }); it('flags hard errors: wrong week count, bad duration, unknown topic_id', () => { const topics = [makeTopic('t1', 'A')]; const schedule = [week(1, 'A', ['t1'])]; const result = validateSchedule(schedule, topics); expect(result.valid).toBe(false); expect(result.errors[0]).toMatch(/exactly 26 weeks/); const fullSchedule = Array.from({ length: 26 }, (_, i) => week(i + 1, 'A', ['t1'])); fullSchedule[2].estimated_duration = 5; fullSchedule[5].topic_ids = ['missing-id']; const result2 = validateSchedule(fullSchedule, topics); expect(result2.valid).toBe(false); expect(result2.errors.some(e => e.includes('out-of-range duration'))).toBe(true); expect(result2.errors.some(e => e.includes('unknown topic_id'))).toBe(true); }); });