| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166 |
- import assert from 'node:assert/strict';
- import { readFileSync } from 'node:fs';
- import { reviewModules, verifyReview } from './review-content.mjs';
- import {
- emptyReviewState,
- restoreReviewState,
- reviewProgress,
- reviewSignature,
- } from '../app/review-state.ts';
- const read = (path) => JSON.parse(readFileSync(path, 'utf8'));
- const lessons = read('public/content/review.json');
- const sources = new Map(
- reviewModules.map((slug) => [
- slug,
- read(slug === 'api' ? 'app/lessons.json' : `public/content/${slug}.json`),
- ]),
- );
- assert.equal(lessons.length, 9);
- const markdown = readFileSync('public/downloads/review-lessons.md', 'utf8');
- let items = 0;
- let criteria = 0;
- for (const [index, lesson] of lessons.entries()) {
- assert.deepEqual(verifyReview(lesson, index, sources), []);
- const start = markdown.indexOf(`## ${lesson.id} ${lesson.title}\n`);
- const end =
- index < lessons.length - 1
- ? markdown.indexOf(
- `## ${lessons[index + 1].id} ${lessons[index + 1].title}\n`,
- start + 1,
- )
- : markdown.length;
- assert(start >= 0 && end > start, `missing review/${lesson.id} download`);
- const text = markdown.slice(start, end);
- const headings = [
- '### 如何自测',
- '### 核心理解问题',
- '### 情境推演',
- '### 掌握标准与证据',
- '### 本轮结果与回学计划',
- '### 参考要点与易错点',
- ];
- const positions = headings.map((heading) => text.indexOf(heading));
- assert(
- positions.every(
- (position, i) => position >= 0 && (!i || position > positions[i - 1]),
- ),
- 'questions must precede reference answers',
- );
- for (const item of [...lesson.questions, ...lesson.challenges]) {
- assert(
- text.slice(0, positions[5]).includes(item.prompt),
- `${item.id}: missing question`,
- );
- for (const point of item.checkpoints)
- assert(
- text.slice(positions[5]).includes(point),
- `${item.id}: missing answer point`,
- );
- assert(text.slice(positions[5]).includes(item.misconception));
- for (const link of item.revisit)
- assert(text.includes(`${link.lessonId} · ${link.label}`));
- items++;
- }
- for (const item of lesson.mastery) {
- assert(text.includes(item.text) && text.includes(item.evidence));
- criteria++;
- }
- const empty = emptyReviewState();
- assert.equal(
- reviewProgress(lesson, empty).mastered,
- false,
- 'opening a review does not mean mastery',
- );
- const answered = {
- ...empty,
- answers: Object.fromEntries(
- [...lesson.questions, ...lesson.challenges].map((item) => [
- item.id,
- 'ready',
- ]),
- ),
- };
- assert.equal(
- reviewProgress(lesson, answered).mastered,
- false,
- 'self-rated answers alone cannot pass the evidence gate',
- );
- const complete = {
- ...answered,
- evidence: Object.fromEntries(lesson.mastery.map((item) => [item.id, true])),
- notes: { q1: '我的判断与证据' },
- };
- assert.equal(reviewProgress(lesson, complete).mastered, true);
- for (const item of [...lesson.questions, ...lesson.challenges]) {
- assert.equal(
- reviewProgress(lesson, {
- ...complete,
- answers: { ...complete.answers, [item.id]: 'revisit' },
- }).mastered,
- false,
- 'one unresolved item must keep the module incomplete',
- );
- }
- assert.equal(
- reviewProgress(lesson, {
- ...complete,
- evidence: { ...complete.evidence, m1: false },
- }).mastered,
- false,
- );
- const raw = JSON.stringify({
- sourceModule: lesson.sourceModule,
- signature: reviewSignature(lesson),
- state: complete,
- });
- assert.deepEqual(
- restoreReviewState(raw, lesson),
- complete,
- 'answers and notes must survive reload',
- );
- const other = lessons[(index + 1) % lessons.length];
- assert.deepEqual(
- restoreReviewState(raw, other),
- emptyReviewState(),
- 'ratings must not leak between modules',
- );
- assert.deepEqual(
- restoreReviewState(raw, {
- ...lesson,
- summary: lesson.summary + ' changed',
- }),
- complete,
- 'editorial summaries must not invalidate self-ratings',
- );
- const changed = restoreReviewState(raw, {
- ...lesson,
- questions: lesson.questions.map((item, i) =>
- i === 0 ? { ...item, prompt: item.prompt + ' changed' } : item,
- ),
- });
- assert.deepEqual(
- changed,
- { ...emptyReviewState(), notes: complete.notes, contentChanged: true },
- 'changed questions preserve notes but require fresh self-ratings',
- );
- assert.deepEqual(restoreReviewState('{broken', lesson), emptyReviewState());
- const invalid = JSON.stringify({
- sourceModule: lesson.sourceModule,
- signature: reviewSignature(lesson),
- state: {
- answers: { q1: 'maybe' },
- notes: { q1: 123 },
- evidence: { m1: 'true' },
- },
- });
- assert.deepEqual(
- restoreReviewState(invalid, lesson),
- emptyReviewState(),
- 'invalid saved fields cannot count as evidence',
- );
- }
- console.log(
- `Review: ${lessons.length} units, ${items} questions/challenges, ${criteria} mastery criteria; return links, download order, evidence gate, persistence, isolation and corrupt storage passed.`,
- );
|