You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Part of #1946 — Redux → React Query migration (Stage 1). Part of the #1976 courseware decomposition (plan) — Target 4 (remaining writers). Stacked on the metadata + sequence conversions (models already exist; no new peel).
Goal: convert the two remaining courseware write thunks to React Query mutations.
Tasks
saveIntegritySignature (HonorCode.jsx) → useMutation; on success patch coursewareMeta.userNeedsIntegritySignature. Preserve the masquerade skip (no backend POST when masquerading).
saveSequencePosition (CoursewareContainer) → useMutation; preserve the optimistic sequences.activeUnitIndex update + rollback on error.
Verify: honor-code acceptance dismisses the modal and clears the integrity prompt; sequence position persists across reloads; rollback on failure.
Plan
Note
The findings and plan below were generated by Claude (Claude Code) and reviewed before posting.
Investigation findings that adjust the task list above:
One stack layer, not a Convert bookmarking to React Query + de-class UnitButton #2014-style split. Both conversions are the same kind of change in the same files — courseware/data/apiHooks.ts gains two mutations, and courseware/data/thunks.js / redux.test.js shrink and die together — so there's nothing to peel and no benefit to splitting; a single PR closes this issue.
The models stay the write target until Dissolve the model-store normalized cache #1977. Every reader of the written state is a model read (sequences.activeUnitIndex via CoursewareContainer's selectors and useIFrameBehavior; coursewareMeta.userNeedsIntegritySignature via useShouldDisplayHonorCode's useModel), so both mutations keep dispatching updateModel rather than patching the sequence/metadata query caches — the same correction verified for Convert bookmarking to React Query + de-class UnitButton #2014: the bridge rewrites models only on a real fetch onSuccess, and a real refetch carries server truth including the saved position / signature. (One pre-existing caveat, unchanged by the port: after a masqueraded dismissal the server still reports the signature as needed, so a metadata refetch can resurrect the prompt — the thunk behaved identically.)
The thunk's getState() rollback pre-read becomes onMutate context.onMutate reads the current activeUnitIndex, writes the optimistic value (synchronously, before the POST — same timing as the thunk's first dispatch), and returns the old index as React Query context for onError to revert to. That's the RQ-native home for rollback state, and it keeps the returned callback's signature identical to the thunk's.
The redundant success re-write is kept.saveSequencePosition writes the same value again after the POST settles ("update again under the assumption that the above call succeeded, since it doesn't return a meaningful response"). Dropping it would change interleaving behavior under rapid unit switches, so the faithful port keeps it, comment and all.
The masquerade skip becomes a resolve-without-request mutationFn branch. When masquerading as a specific learner, mutationFn resolves without posting, so onSuccess still dismisses the honor-code prompt — 1:1 with the thunk's if (!isMasquerading) guard ahead of its unconditional dispatch. On failure: logError only, no model write, the prompt stays (the thunk's catch path).
useDispatch leaves CoursewareContainer entirely — saveSequencePosition was its last use. Capturing the hook's callback in the once-created guards.current closure is safe because React Query's mutate is a stable reference.
redux.test.js is deleted, not trimmed — its three cases are exactly these two thunks. They port to apiHooks.test.tsx, plus three new cases: the 1-indexed { position: index + 1 } wire body (the old test only asserted the URL), a mid-flight optimistic write (hanging mock), and the integrity-signature failure path, which had no coverage at all. Pact tests call the raw api functions and are untouched; HonorCode.test.jsx's four cases already pin the component behavior and stay as-is.
No behavior changes intended — same optimistic write timing, same revert values, same logError calls, same masquerade skip, fire-and-forget at both call sites.
Part of epic #1946 (Redux → React Query, Stage 1) and the #1976 courseware
decomposition — Target 4 (remaining writers). These are the last two
courseware write thunks (src/courseware/data/thunks.js); converting them
leaves only getCourseDiscussionTopics (#2016) between here and the #1976
slice teardown. The issue is right that no peel is needed — both models
(sequences, coursewareMeta) already exist and both call sites are already
function components.
Unlike #2014's bookmark split, the two tasks here are the same kind of change
in the same files (courseware/data/apiHooks.ts gains two mutations; courseware/data/thunks.js + redux.test.js shrink/die together), so this
ships as one stack layer on top of #2066 (the bookmark conversion),
closing this issue.
The two thunks and their call sites:
saveSequencePosition(courseId, sequenceId, activeUnitIndex) —
optimistic updateModel({ modelType: 'sequences', … activeUnitIndex }), postSequencePosition (1-indexed on the wire: position: activeUnitIndex + 1),
then a second identicalupdateModel on success ("update again under the
assumption that the above call succeeded, since it doesn't return a
meaningful response"), or logError + revert to the pre-read initialActiveUnitIndex on failure. Sole dispatcher: CoursewareContainer.tsx:255, inside the guards.current.checkSaveSequencePosition
memoized closure (fires on route-unit change when sequence.saveUnitPosition — normalized from save_position — is set).
saveIntegritySignature(courseId, isMasquerading) — when not
masquerading-as-a-specific-learner, postIntegritySignature; in both cases
on success updateModel({ modelType: 'coursewareMeta', … userNeedsIntegritySignature: false }); on failure logError only (no model
write — the honor-code prompt stays). Sole dispatcher: HonorCode.jsx:28
(handleAgree). The masquerade predicate
(isMasquerading && username !== authUser.username) is computed by the
component and stays there.
Readers of the written state (all via models; none change):
sequences.activeUnitIndex: CoursewareContainer's checkSequenceToSequenceUnitRedirect (via currentSequenceSelector) and useIFrameBehavior.ts:45 (via the active-sequence model read).
Model store stays the write target — same correction as #2014 Layer B: the
readers are model-store reads, which remain the merged source of truth until #1977, so both mutations keep dispatching updateModel, not setQueryData on
the sequence/metadata query caches. Durability holds the same way: the bridge
(src/data/modelStoreBridge.ts) rewrites sequences/coursewareMeta only on
a real fetch onSuccess, and a real refetch carries server truth including
the saved position / signature. (Masquerade caveat: after a masqueraded
dismissal the server still reports user_needs_integrity_signature: true, so
a metadata refetch can resurrect the prompt — byte-identical to the thunk
behavior, since a re-fetch overwrote the Redux state the same way. Not a
regression; the dismissal was always session-scoped.) Query-cache patching is
deferred to #1977, as noted there for bookmarks.
1. courseware/data/apiHooks.ts — two mutations
Both go in this file alongside useCheckBlockCompletion (these are courseware/data thunks — no feature subdirectory of their own, unlike
bookmarks). No mutation keys, explicit logError in onError (mutations
aren't covered by the global QueryCache.onError), return a callback rather
than the mutation object — all per the useCheckBlockCompletion / useSetBookmarked precedent.
useSaveSequencePosition
interfaceSaveSequencePositionVars{courseId: string|null;// from the still-untyped Redux slice via latest.current (#1976)sequenceId: string|null;activeUnitIndex: number;}exportconstuseSaveSequencePosition=()=>{conststore=useStore();constdispatch=useDispatch();constsetPosition=(sequenceId: string|null,activeUnitIndex: number)=>{dispatch(updateModel({modelType: 'sequences',model: {id: sequenceId, activeUnitIndex }}));};const{ mutate }=useMutation({mutationFn: ({ courseId, sequenceId, activeUnitIndex }: SaveSequencePositionVars)=>(postSequencePosition(courseId,sequenceId,activeUnitIndex)),// Optimistically update the position; remember the old one for rollback.onMutate: ({ sequenceId, activeUnitIndex })=>{const{ models }=store.getState()as{models: {sequences: Record<string,{activeUnitIndex: number}>}};constinitialActiveUnitIndex=models.sequences[sequenceId].activeUnitIndex;setPosition(sequenceId,activeUnitIndex);return{ initialActiveUnitIndex };},// Update again under the assumption that the call succeeded, since it// doesn't return a meaningful response.onSuccess: (_data,{ sequenceId, activeUnitIndex })=>setPosition(sequenceId,activeUnitIndex),onError: (error,{ sequenceId },context)=>{logError(error);setPosition(sequenceId,context.initialActiveUnitIndex);},});returnuseCallback((courseId,sequenceId,activeUnitIndex)=>{mutate({ courseId, sequenceId, activeUnitIndex });},[mutate]);};
(Exact spelling — including the store.getState() cast shape and callback
param types — at implementation time.)
The thunk's getState() pre-read maps onto onMutate returning RQ
context consumed by onError — the React Query-native home for
rollback state; keeps the returned callback's signature identical to the
thunk's. useStore (not a selector) for the read, per the useCheckBlockCompletion guard precedent.
onMutate runs synchronously before mutationFn, so the optimistic write
lands before the POST exactly as the thunk's first dispatch did.
The redundant-looking success re-write is kept, with its original
comment — faithful port (it re-asserts the position after settle; dropping
it would change interleaving behavior under rapid unit switches).
useSaveIntegritySignature
interfaceSaveIntegritySignatureVars{courseId: string;isMasquerading: boolean;}exportconstuseSaveIntegritySignature=()=>{constdispatch=useDispatch();const{ mutate }=useMutation({// If the request is made by a staff user masquerading as a specific learner,// don't actually create a signature for them on the backend,// only the modal dialog will be dismissedmutationFn: async({ courseId, isMasquerading }: SaveIntegritySignatureVars)=>(isMasquerading ? null : postIntegritySignature(courseId)),onSuccess: (_data,{ courseId })=>{dispatch(updateModel({modelType: 'coursewareMeta',model: {id: courseId,userNeedsIntegritySignature: false}}));},onError: (error)=>logError(error),});returnuseCallback((courseId,isMasquerading)=>{mutate({ courseId, isMasquerading });},[mutate]);};
The masquerade skip lives in mutationFn (resolve without a request → onSuccess still fires → prompt dismissed), mapping 1:1 onto the thunk's if (!isMasquerading) guard ahead of the unconditional dispatch.
onError: logError only, no model write — the thunk's catch path.
2. CoursewareContainer.tsx
const saveSequencePosition = useSaveSequencePosition(); (rename-on-read),
so line 255 becomes saveSequencePosition(cId, sId, activeUnitIndex); —
the dispatch(…) wrapper is the only change to the guard body.
Safe to capture in the once-created guards.current closure: RQ's mutate
is a stable reference and store is stable, so the useCallback result
never goes stale.
useDispatch leaves the file entirely (this was its last use); saveSequencePosition drops out of the ./data import and useSaveSequencePosition joins the ./data/apiHooks import next to useCheckBlockCompletion.
3. HonorCode.jsx
Drop useDispatch + the saveIntegritySignature import from ../../../data; const saveIntegritySignature = useSaveIntegritySignature(); (rename-on-read).
handleAgree keeps its shape and its masquerade comment: () => saveIntegritySignature(courseId, isMasquerading && username !== authUser.username).
File stays .jsx — minimal diff, no structural change (the TS-for-new-files
rule applies to new files only).
4. Cleanup
courseware/data/thunks.js: both thunks deleted; getCourseDiscussionTopics
remains (dies in Convert getCourseDiscussionTopics to React Query #2016). Trim now-unused imports (updateModel, postIntegritySignature, postSequencePosition).
courseware/data/index.js: the whole from './thunks' export block goes
(it exports exactly these two); api/selectors/slice exports stay.
courseware/data/redux.test.js: deleted — its three cases (all on these
two thunks; nothing else in the file) port to apiHooks.test.tsx.
Tests
courseware/data/apiHooks.test.tsx — two new describes on the existing
mutation pattern (renderHook + AppProvider store × QueryClientProvider
via createTestQueryClient, seedSequenceModels for the sequence cases,
axios-mock on the handler URLs):
useSaveSequencePosition:
success → models.sequences[sequenceId].activeUnitIndex updated; POST to the goto_position handler URL with body { position: newIndex + 1 } (the body
assertion is new — redux.test.js only checked the URL; it pins the
1-indexed wire format the api comment promises).
network error → logError called, activeUnitIndex reverted to the seeded
value.
success → userNeedsIntegritySignature flips true → false, POST to the
agreements URL.
masquerading (new at hook level; component-level coverage exists in HonorCode.test.jsx) → no request recorded, flag still flips to false.
failure (new — the thunk's error path was untested): POST network error → logError called, flag stays true.
HonorCode.test.jsx — expected green as-is: renders through setupTest's render (store + query client both provided), keeps its axios mocks, and its
four cases assert exactly the behavior the hook preserves (navigate on cancel,
POST when agreeing, POST when generally masquerading, no POST when
masquerading a specific student). No fireEvent → userEvent churn unless the
file needs touching anyway.
CoursewareContainer.test.jsx — no goto_position coverage exists;
no edits expected, verify green.
Pact tests (lmsPact.test.jsx) — call postSequencePosition /
the api layer directly; untouched.
Behavior changes
None intended — faithful ports: same optimistic write timing, same
revert-on-error with the pre-mutation index, same success re-write, same logError calls, same masquerade skip, same no-write-on-failure for the
signature, fire-and-forget at both call sites.
Guard-closure capture in CoursewareContainer is safe (mutate stable).
redux.test.js deleted (nothing left in it); new hook-level cases:
1-indexed body, mid-flight optimistic write, masquerade skip,
integrity-failure path.
Stack
One new layer stacked on #2066 (the bookmark conversion), closing this issue.
Submitted once green.
Verification
npm run test -- src/courseware/data src/courseware/course/sequence/honor-code src/courseware/CoursewareContainer.
npm run types and npm run lint.
Manual smoke (tutor local, DemoX): sequence position — open a sequence whose save_position is set (exam-style subsections; the Demo Course "Homework -
Question Styles" sequence reports save_position: false, so pick/author one
that saves), click through units, watch goto_position POSTs in the Network
tab, reload → lands on the last-active unit; offline toggle → position
reverts + console error. Honor code — needs a course with the integrity
signature feature enabled (user_needs_integrity_signature: true on
courseware metadata); if no local course qualifies, the hook-level + HonorCode.test.jsx coverage carries it, noted in the manual-testing doc.
Part of #1946 — Redux → React Query migration (Stage 1). Part of the #1976 courseware decomposition (plan) — Target 4 (remaining writers). Stacked on the metadata + sequence conversions (models already exist; no new peel).
Goal: convert the two remaining courseware write thunks to React Query mutations.
Tasks
saveIntegritySignature(HonorCode.jsx) →useMutation; on success patchcoursewareMeta.userNeedsIntegritySignature. Preserve the masquerade skip (no backend POST when masquerading).saveSequencePosition(CoursewareContainer) →useMutation; preserve the optimisticsequences.activeUnitIndexupdate + rollback on error.Verify: honor-code acceptance dismisses the modal and clears the integrity prompt; sequence position persists across reloads; rollback on failure.
Plan
Note
The findings and plan below were generated by Claude (Claude Code) and reviewed before posting.
Investigation findings that adjust the task list above:
courseware/data/apiHooks.tsgains two mutations, andcourseware/data/thunks.js/redux.test.jsshrink and die together — so there's nothing to peel and no benefit to splitting; a single PR closes this issue.sequences.activeUnitIndexviaCoursewareContainer's selectors anduseIFrameBehavior;coursewareMeta.userNeedsIntegritySignatureviauseShouldDisplayHonorCode'suseModel), so both mutations keep dispatchingupdateModelrather than patching the sequence/metadata query caches — the same correction verified for Convert bookmarking to React Query + de-class UnitButton #2014: the bridge rewrites models only on a real fetchonSuccess, and a real refetch carries server truth including the saved position / signature. (One pre-existing caveat, unchanged by the port: after a masqueraded dismissal the server still reports the signature as needed, so a metadata refetch can resurrect the prompt — the thunk behaved identically.)getState()rollback pre-read becomesonMutatecontext.onMutatereads the currentactiveUnitIndex, writes the optimistic value (synchronously, before the POST — same timing as the thunk's first dispatch), and returns the old index as React Query context foronErrorto revert to. That's the RQ-native home for rollback state, and it keeps the returned callback's signature identical to the thunk's.saveSequencePositionwrites the same value again after the POST settles ("update again under the assumption that the above call succeeded, since it doesn't return a meaningful response"). Dropping it would change interleaving behavior under rapid unit switches, so the faithful port keeps it, comment and all.mutationFnbranch. When masquerading as a specific learner,mutationFnresolves without posting, soonSuccessstill dismisses the honor-code prompt — 1:1 with the thunk'sif (!isMasquerading)guard ahead of its unconditional dispatch. On failure:logErroronly, no model write, the prompt stays (the thunk's catch path).useDispatchleavesCoursewareContainerentirely —saveSequencePositionwas its last use. Capturing the hook's callback in the once-createdguards.currentclosure is safe because React Query'smutateis a stable reference.redux.test.jsis deleted, not trimmed — its three cases are exactly these two thunks. They port toapiHooks.test.tsx, plus three new cases: the 1-indexed{ position: index + 1 }wire body (the old test only asserted the URL), a mid-flight optimistic write (hanging mock), and the integrity-signature failure path, which had no coverage at all. Pact tests call the raw api functions and are untouched;HonorCode.test.jsx's four cases already pin the component behavior and stay as-is.logErrorcalls, same masquerade skip, fire-and-forget at both call sites.Full plan
Plan: #2015 — Convert saveIntegritySignature + saveSequencePosition to React Query mutations
Context
Part of epic #1946 (Redux → React Query, Stage 1) and the #1976 courseware
decomposition — Target 4 (remaining writers). These are the last two
courseware write thunks (
src/courseware/data/thunks.js); converting themleaves only
getCourseDiscussionTopics(#2016) between here and the #1976slice teardown. The issue is right that no peel is needed — both models
(
sequences,coursewareMeta) already exist and both call sites are alreadyfunction components.
Unlike #2014's bookmark split, the two tasks here are the same kind of change
in the same files (
courseware/data/apiHooks.tsgains two mutations;courseware/data/thunks.js+redux.test.jsshrink/die together), so thisships as one stack layer on top of #2066 (the bookmark conversion),
closing this issue.
The two thunks and their call sites:
saveSequencePosition(courseId, sequenceId, activeUnitIndex)—optimistic
updateModel({ modelType: 'sequences', … activeUnitIndex }),postSequencePosition(1-indexed on the wire:position: activeUnitIndex + 1),then a second identical
updateModelon success ("update again under theassumption that the above call succeeded, since it doesn't return a
meaningful response"), or
logError+ revert to the pre-readinitialActiveUnitIndexon failure. Sole dispatcher:CoursewareContainer.tsx:255, inside theguards.current.checkSaveSequencePositionmemoized closure (fires on route-unit change when
sequence.saveUnitPosition— normalized fromsave_position— is set).saveIntegritySignature(courseId, isMasquerading)— when notmasquerading-as-a-specific-learner,
postIntegritySignature; in both caseson success
updateModel({ modelType: 'coursewareMeta', … userNeedsIntegritySignature: false }); on failurelogErroronly (no modelwrite — the honor-code prompt stays). Sole dispatcher:
HonorCode.jsx:28(
handleAgree). The masquerade predicate(
isMasquerading && username !== authUser.username) is computed by thecomponent and stays there.
Readers of the written state (all via models; none change):
sequences.activeUnitIndex:CoursewareContainer'scheckSequenceToSequenceUnitRedirect(viacurrentSequenceSelector) anduseIFrameBehavior.ts:45(via the active-sequence model read).coursewareMeta.userNeedsIntegritySignature:useShouldDisplayHonorCode.js:13(useModel).Model store stays the write target — same correction as #2014 Layer B: the
readers are model-store reads, which remain the merged source of truth until
#1977, so both mutations keep dispatching
updateModel, notsetQueryDataonthe sequence/metadata query caches. Durability holds the same way: the bridge
(
src/data/modelStoreBridge.ts) rewritessequences/coursewareMetaonly ona real fetch
onSuccess, and a real refetch carries server truth includingthe saved position / signature. (Masquerade caveat: after a masqueraded
dismissal the server still reports
user_needs_integrity_signature: true, soa metadata refetch can resurrect the prompt — byte-identical to the thunk
behavior, since a re-fetch overwrote the Redux state the same way. Not a
regression; the dismissal was always session-scoped.) Query-cache patching is
deferred to #1977, as noted there for bookmarks.
1.
courseware/data/apiHooks.ts— two mutationsBoth go in this file alongside
useCheckBlockCompletion(these arecourseware/datathunks — no feature subdirectory of their own, unlikebookmarks). No mutation keys, explicit
logErrorinonError(mutationsaren't covered by the global
QueryCache.onError), return a callback ratherthan the mutation object — all per the
useCheckBlockCompletion/useSetBookmarkedprecedent.useSaveSequencePosition(Exact spelling — including the
store.getState()cast shape and callbackparam types — at implementation time.)
getState()pre-read maps ontoonMutatereturning RQcontext consumed by
onError— the React Query-native home forrollback state; keeps the returned callback's signature identical to the
thunk's.
useStore(not a selector) for the read, per theuseCheckBlockCompletionguard precedent.onMutateruns synchronously beforemutationFn, so the optimistic writelands before the POST exactly as the thunk's first dispatch did.
comment — faithful port (it re-asserts the position after settle; dropping
it would change interleaving behavior under rapid unit switches).
useSaveIntegritySignaturemutationFn(resolve without a request →onSuccessstill fires → prompt dismissed), mapping 1:1 onto the thunk'sif (!isMasquerading)guard ahead of the unconditional dispatch.onError:logErroronly, no model write — the thunk's catch path.2.
CoursewareContainer.tsxconst saveSequencePosition = useSaveSequencePosition();(rename-on-read),so line 255 becomes
saveSequencePosition(cId, sId, activeUnitIndex);—the
dispatch(…)wrapper is the only change to the guard body.guards.currentclosure: RQ'smutateis a stable reference and
storeis stable, so theuseCallbackresultnever goes stale.
useDispatchleaves the file entirely (this was its last use);saveSequencePositiondrops out of the./dataimport anduseSaveSequencePositionjoins the./data/apiHooksimport next touseCheckBlockCompletion.3.
HonorCode.jsxuseDispatch+ thesaveIntegritySignatureimport from../../../data;const saveIntegritySignature = useSaveIntegritySignature();(rename-on-read).handleAgreekeeps its shape and its masquerade comment:() => saveIntegritySignature(courseId, isMasquerading && username !== authUser.username)..jsx— minimal diff, no structural change (the TS-for-new-filesrule applies to new files only).
4. Cleanup
courseware/data/thunks.js: both thunks deleted;getCourseDiscussionTopicsremains (dies in Convert getCourseDiscussionTopics to React Query #2016). Trim now-unused imports (
updateModel,postIntegritySignature,postSequencePosition).courseware/data/index.js: the wholefrom './thunks'export block goes(it exports exactly these two); api/selectors/slice exports stay.
courseware/data/redux.test.js: deleted — its three cases (all on thesetwo thunks; nothing else in the file) port to
apiHooks.test.tsx.Tests
courseware/data/apiHooks.test.tsx— two new describes on the existingmutation pattern (
renderHook+AppProvider store×QueryClientProvidervia
createTestQueryClient,seedSequenceModelsfor the sequence cases,axios-mock on the handler URLs):
useSaveSequencePosition:models.sequences[sequenceId].activeUnitIndexupdated; POST to thegoto_positionhandler URL with body{ position: newIndex + 1 }(the bodyassertion is new —
redux.test.jsonly checked the URL; it pins the1-indexed wire format the api comment promises).
logErrorcalled,activeUnitIndexreverted to the seededvalue.
activeUnitIndexis already the new value before the request resolves.useSaveIntegritySignature:userNeedsIntegritySignatureflipstrue → false, POST to theagreements URL.
HonorCode.test.jsx) → no request recorded, flag still flips tofalse.logErrorcalled, flag staystrue.HonorCode.test.jsx— expected green as-is: renders throughsetupTest'srender(store + query client both provided), keeps its axios mocks, and itsfour cases assert exactly the behavior the hook preserves (navigate on cancel,
POST when agreeing, POST when generally masquerading, no POST when
masquerading a specific student). No
fireEvent → userEventchurn unless thefile needs touching anyway.
CoursewareContainer.test.jsx— nogoto_positioncoverage exists;no edits expected, verify green.
Pact tests (
lmsPact.test.jsx) — callpostSequencePosition/the api layer directly; untouched.
Behavior changes
None intended — faithful ports: same optimistic write timing, same
revert-on-error with the pre-mutation index, same success re-write, same
logErrorcalls, same masquerade skip, same no-write-on-failure for thesignature, fire-and-forget at both call sites.
Decision doc
For the PR body at submit time:
Convert bookmarking to React Query + de-class UnitButton #2014 peel+convert split).
courseware/data/apiHooks.ts(no feature subdirectory —these are
courseware/datathunks, unlike bookmarks).onMutate-context rollback for the sequence position (vs pre-reading in thereturned callback) — RQ-native; callback signature matches the thunk.
onSuccessre-write kept, comment and all (faithful port).mutationFnbranch.setQueryData; bridge durability;masquerade-refetch caveat is pre-existing) — deferred to Dissolve the model-store normalized cache #1977.
CoursewareContaineris safe (mutatestable).redux.test.jsdeleted (nothing left in it); new hook-level cases:1-indexed body, mid-flight optimistic write, masquerade skip,
integrity-failure path.
Stack
One new layer stacked on #2066 (the bookmark conversion), closing this issue.
Submitted once green.
Verification
npm run test -- src/courseware/data src/courseware/course/sequence/honor-code src/courseware/CoursewareContainer.npm run typesandnpm run lint.save_positionis set (exam-style subsections; the Demo Course "Homework -Question Styles" sequence reports
save_position: false, so pick/author onethat saves), click through units, watch
goto_positionPOSTs in the Networktab, reload → lands on the last-active unit; offline toggle → position
reverts + console error. Honor code — needs a course with the integrity
signature feature enabled (
user_needs_integrity_signature: trueoncourseware metadata); if no local course qualifies, the hook-level +
HonorCode.test.jsxcoverage carries it, noted in the manual-testing doc.