Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@
"@vitejs/plugin-react": "6.0.4",
"@vitest/coverage-v8": "4.1.10",
"@vitest/ui": "4.1.10",
"happy-dom": "20.11.1",
"happy-dom": "21.0.4",
"husky": "^9.1.7",
"jscpd": "5.0.14",
"knip": "^6.29.0",
Expand Down Expand Up @@ -133,12 +133,13 @@
"nanoid@>=4.0.0 <5.1.16": "5.1.16",
"postcss@<=8.5.22": "8.5.23",
"undici@>=5.0.0 <6.27.0": "6.29.0",
"undici@>=7.0.0 <7.29.1": "7.29.1",
"undici@>=7.0.0 <7.30.1": "7.30.1",
"sharp@<0.35.5": "0.35.5",
"js-yaml@>=4.0.0 <4.3.2": "4.3.2",
"smol-toml@<1.7.1": "1.7.1",
"source-map-js@<1.2.2": "1.2.2",
"dompurify@<3.4.17": "3.4.17"
"dompurify@<3.4.17": "3.4.17",
"zod@>=4.0.0 <4.6.6": "4.6.6"
},
"peerDependencyRules": {
"allowAny": [
Expand Down
77 changes: 39 additions & 38 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/lib/contentQuality.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export const DRILL_VERIFICATION_HINT: Record<DrillVerification, string> = {
'outline-check':
'Only the shape of your outline is checked — that every required point is answered in your own words. Nobody grades whether it is right, so mastery moves less than an auto-graded drill.',
'self-reported':
'Nothing is verified in-app. Marking this solved is your own claim, and it moves mastery less than an auto-graded drill.',
'Nothing is verified in-app. Marking this solved records your claim only; pass the explain-back to update mastery.',
};

export function isEditorialDrill(drill: Drill): boolean {
Expand Down
44 changes: 39 additions & 5 deletions src/pages/BuildLab.persistence.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import { DRILL_BY_ID } from '../data/learning-os';
import { STORE_KEYS } from '../lib/userStore';

const auth = vi.hoisted(() => ({ user: null as null | { id: string } }));
const mastery = vi.hoisted(() => ({ review: vi.fn(), refresh: vi.fn() }));
const reviewMastery = vi.hoisted(() => ({ review: vi.fn(), mastery: {} }));
const elo = vi.hoisted(() => ({ recordResult: vi.fn() }));
const suspendedQuestions = vi.hoisted(() => ({ unsuspend: vi.fn() }));
vi.mock('../contexts/AuthContext', () => ({ useAuth: () => ({ user: auth.user }) }));
vi.mock('../components/CodeEditor', () => ({
default: ({ code, onChange }: { code: string; onChange: (value: string) => void }) => (
Expand All @@ -22,18 +26,23 @@ vi.mock('../components/CodeEditor', () => ({
vi.mock('../components/FeynmanGate', () => ({ default: () => null }));
vi.mock('../components/FeynmanNudge', () => ({ default: () => null }));
vi.mock('../hooks/useConcepts', () => ({
useConceptMastery: () => ({ mastery: {}, refresh: vi.fn(), review: vi.fn() }),
useConceptMastery: () => ({ mastery: {}, refresh: mastery.refresh, review: mastery.review }),
}));
vi.mock('../hooks/useReviewMastery', () => ({
useReviewMastery: () => ({ mastery: {}, review: vi.fn() }),
useReviewMastery: () => reviewMastery,
}));
vi.mock('../lib/reviewMastery', async (importOriginal) => ({
...(await importOriginal<typeof import('../lib/reviewMastery')>()),
loadSuspendedRqs: () => new Set(['rq-array-hashing-core', 'rq-tokenization-symmetry']),
unsuspendReviewQuestion: suspendedQuestions.unsuspend,
}));
vi.mock('../hooks/useActivity', () => ({ useActivityLogger: () => vi.fn() }));
vi.mock('../hooks/useCodeExecution', () => ({
useCodeExecution: () => ({ output: '', errors: '', isRunning: false, execute: vi.fn() }),
}));
vi.mock('../hooks/useUserStore', async (importOriginal) => ({
...(await importOriginal<typeof import('../hooks/useUserStore')>()),
useUserElo: () => ({ recordResult: vi.fn() }),
useUserElo: () => elo,
}));

const storage = new Map<string, string>();
Expand All @@ -47,6 +56,7 @@ Object.defineProperty(globalThis, 'localStorage', {
});

const drill = DRILL_BY_ID['build-tokenizer'];
const unverifiedDrill = DRILL_BY_ID['lc-two-sum'];
const nextDrill = Object.values(DRILL_BY_ID).find(
(item) => item.id !== drill.id && item.testCases?.length
)!;
Expand All @@ -72,10 +82,10 @@ describe('drill grading and saved exercise resume', () => {
button.textContent?.includes('Mark solved')
)!;

async function render() {
async function render(drillId = drill.id) {
await act(async () =>
root.render(
<MemoryRouter initialEntries={[`/build/${drill.id}`]}>
<MemoryRouter initialEntries={[`/build/${drillId}`]}>
<Link to={`/build/${nextDrill.id}`}>Next exercise</Link>
<Routes>
<Route path="/build/:id" element={<BuildLab />} />
Expand All @@ -89,6 +99,11 @@ describe('drill grading and saved exercise resume', () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
storage.clear();
auth.user = null;
mastery.review.mockClear();
mastery.refresh.mockClear();
reviewMastery.review.mockClear();
elo.recordResult.mockClear();
suspendedQuestions.unsuspend.mockClear();
container = document.createElement('div');
document.body.append(container);
root = createRoot(container);
Expand All @@ -107,6 +122,10 @@ describe('drill grading and saved exercise resume', () => {
expect(container.textContent).not.toContain('All tests passed.');
await act(async () => typeCode(editor(), drill.referenceSolution!));
await act(async () => click(solve()));
expect(mastery.review).toHaveBeenCalledWith(drill.conceptId, 'easy');
expect(reviewMastery.review).toHaveBeenCalled();
expect(elo.recordResult).toHaveBeenCalledOnce();
expect(suspendedQuestions.unsuspend).toHaveBeenCalledWith('rq-tokenization-symmetry');
const saved = JSON.parse(storage.get(STORE_KEYS.drills)!)[drill.id];
expect(saved).toEqual({ status: 'solved', lastCode: drill.referenceSolution, attempts: 1 });
expect(container.textContent).toContain('All tests passed.');
Expand All @@ -129,6 +148,21 @@ describe('drill grading and saved exercise resume', () => {
expect(container.textContent).toContain(nextDrill.title);
});

it('keeps self-reported completion out of concept mastery and review scheduling', async () => {
await render(unverifiedDrill.id);
expect(container.textContent).toContain('Honor system');

await act(async () => click(solve()));

expect(JSON.parse(storage.get(STORE_KEYS.drills)!)[unverifiedDrill.id]).toMatchObject({
status: 'solved',
});
expect(mastery.review).not.toHaveBeenCalled();
expect(reviewMastery.review).not.toHaveBeenCalled();
expect(elo.recordResult).not.toHaveBeenCalled();
expect(suspendedQuestions.unsuspend).not.toHaveBeenCalled();
});

it('hydrates account code received after mount without overwriting a learner edit', async () => {
auth.user = { id: 'test-learner' };
let resolveFetch!: (value: Response) => void;
Expand Down
Loading
Loading