fix(create-lesson): strip stray ext_payload from core exercises on edit-mode reconstruction (#1919)#1926
Merged
Conversation
…it-mode reconstruction (#1919) Reopening a saved core-only lesson for editing failed the Review "Valid lesson structure" check with "/steps/1/exercise/ext_payload must be object", although the same lesson was fully valid in the create flow immediately before saving. Root cause (API/server mode): the GET lessons endpoint serves the lesson via response_model=Lesson with FastAPI's default exclude_none=False, so the Pydantic Exercise.ext_payload (dict | None = None) is serialized as ext_payload: null on every core exercise. The lesson JSON-Schema types ext_payload as object-only ("Absent on core exercises"), while every other optional exercise field tolerates null via anyOf. lessonToDraftInput copied the exercise verbatim into the wizard, so the re-validation on the Review step rejected the null. Dexie mode parses via the engine adapter, which omits the key, so it only reproduced in API mode. Fix: reconstructExercise restores the invariant "ext_payload present iff the exercise is an ext: extension type" — a core exercise carries no ext_payload, an extension exercise keeps its real payload. Mode- agnostic (a no-op in Dexie mode). Verified an extension exercise's payload is preserved (no over-correction). Closes #1919
…ory (#1919) The Frontend Tests CI job's `bun audit --audit-level=high` gate went red on a newly-published advisory GHSA-4c8g-83qw-93j6 (fast-uri host confusion via failed IDN canonicalization, affecting >=3.0.0 <3.1.3), pulled transitively through ajv -> fast-uri. Not introduced by this branch's code change; the advisory started failing the audit gate. Add a `fast-uri: ^3.1.3` override (same pattern as the existing qs / brace-expansion security pins) so the resolved version is 3.1.4. ajv schema validation is unaffected (lesson-schema-validator + draft-to-lesson suites green).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Reopening a saved core-only lesson for editing failed the Review "Valid lesson structure" check with
/steps/1/exercise/ext_payload must be object, even though the same lesson was fully green in the create flow immediately before saving.Save changeswas blocked on a lesson the author never touched an extension in.Closes #1919
Root cause (API/server mode only)
The GET lessons endpoint serves the lesson via
response_model=Lessonwith FastAPI's defaultexclude_none=False, so the PydanticExercise.ext_payload: dict | None = Noneserializes asext_payload: nullon every core exercise. Proven directly against the model:The lesson JSON-Schema types
ext_payloadas object-only (type: object, "Absent on core exercises") — while every other optional exercise field tolerates null viaanyOf: [..., {type: null}]. Soext_payloadis the single field that breaks.lessonToDraftInputcopied the reloaded exercise verbatim into the wizard, andcheckDraft → buildLessonFromDraft's ajv validation then rejected the null.Dexie mode parses via the engine adapter (
parseLesson), which omits the key entirely — verified a full generate → save → parse round-trip is clean — so this reproduced only in API mode.Fix
reconstructExercise(inlessonToDraftInput) restores the invariant "ext_payloadpresent iff the exercise is anext:extension type": a core exercise carries noext_payload; an extension exercise keeps its real payload. Mode-agnostic (a no-op in Dexie mode, since the key is already absent there). Scoped precisely toext_payload— the broader regression check confirmed no other optional field breaks the schema the same way.Type of Change
Testing
make testslice passes locally:draft-to-lesson,lib/content/lesson/,lib/exercises/authoring/,CreateLesson, plusstorage/content/+lib/content/sweep (77 files / 916 tests green);tsc --noEmitclean; eslint clean on changed files.draft-to-lesson.test.tsreproduce the API-mode reload (ext_payload: nullon core exercises) — before the fix they failed with the exact/steps/1/exercise/ext_payload must be object; after the fix they pass.ext_payloadis preserved through reconstruction (no over-correction), including in a mixed core+extension lesson.Root cause additionally verified end-to-end at the source (backend Pydantic
Exercise.model_dumpemitsext_payload: None) and at the sink (Dexie engineparseLessonround-trip omits the key).Documentation
Checklist
Generated by Claude Code