When the KB has more than 26 themes the curriculum AI is required to merge — so theme labels not appearing as week names are expected, not warnings. The previous validation surfaced this as a console.warn that read like an error in the learning station console. - validateSchedule now only flags missing theme labels when themes_kb <= 26 - adds a real coverage check: warns when learning topics are absent from every week (the actual signal we care about) - adds vitest coverage for both behaviours Closes #12 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
98 lines
3.4 KiB
JavaScript
98 lines
3.4 KiB
JavaScript
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);
|
|
});
|
|
});
|