Files
OpenSquawk/shared/learn/variantSignature.test.ts
itsrubberduck 2c5c2d5239 feat(classroom): show the full curriculum and raise the mastery bar
The overview rendered four anonymous module tiles and half a page of empty
space, which undersold 63 existing lessons. It now stacks four module
sections that list every lesson as a chip with status and variant dots, and
the header states the real scope: 4 modules, 63 lessons, 262 variations.

Mastery moves from a flat two-variation counter to per-module thresholds
(3/4/4/5) backed by persisted variant signatures — a hash over each roll's
expected answers. That triples the practice runs to full mastery without
authoring a single new lesson, and fixes a dedup hole where the same
variation counted twice after a page reload.

Existing progress is preserved: `done` is sticky, so raising the bar never
revokes a check mark someone already earned.

Also fixes `yarn test`, which passed quoted globs Node 20 does not expand —
it exited 0 without running a single test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 20:08:55 +02:00

56 lines
1.8 KiB
TypeScript

import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import { lessonVariantSignature } from './variantSignature'
import type { Lesson, Scenario } from './types'
function lessonWithFields(keys: string[]): Pick<Lesson, 'fields'> {
return {
fields: keys.map(key => ({
key,
label: key,
expected: (scenario: Scenario) => String((scenario as unknown as Record<string, unknown>)[key] ?? ''),
})),
}
}
const scenarioA = { runway: '25L', squawk: '4711' } as unknown as Scenario
const scenarioB = { runway: '07R', squawk: '4711' } as unknown as Scenario
describe('lessonVariantSignature', () => {
it('is stable for the same scenario', () => {
const lesson = lessonWithFields(['runway', 'squawk'])
assert.equal(
lessonVariantSignature(lesson, scenarioA),
lessonVariantSignature(lesson, scenarioA),
)
})
it('differs when an expected answer differs', () => {
const lesson = lessonWithFields(['runway', 'squawk'])
assert.notEqual(
lessonVariantSignature(lesson, scenarioA),
lessonVariantSignature(lesson, scenarioB),
)
})
it('ignores scenario values the lesson never asks for', () => {
const lesson = lessonWithFields(['squawk'])
assert.equal(
lessonVariantSignature(lesson, scenarioA),
lessonVariantSignature(lesson, scenarioB),
)
})
it('returns a non-empty token for a lesson without fields', () => {
assert.ok(lessonVariantSignature(lessonWithFields([]), scenarioA).length > 0)
})
it('survives a field whose expected() throws', () => {
const lesson = {
fields: [{ key: 'boom', label: 'boom', expected: () => { throw new Error('nope') } }],
} as unknown as Pick<Lesson, 'fields'>
assert.ok(lessonVariantSignature(lesson, scenarioA).length > 0)
})
})