refactor: tear down the courseware Redux slice - #2074
Open
brian-smith-tcril wants to merge 1 commit into
Open
brian-smith-tcril wants to merge 1 commit into
brian-smith-tcril wants to merge 1 commit into
Conversation
brian-smith-tcril
added this pull request to stack #2062
September 17, 2026 00:49
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #2074 +/- ##
==========================================
- Coverage 93.77% 93.64% -0.13%
==========================================
Files 367 366 -1
Lines 6038 5917 -121
Branches 1395 1400 +5
==========================================
- Hits 5662 5541 -121
+ Misses 360 359 -1
- Partials 16 17 +1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
brian-smith-tcril
force-pushed
the
bsmith/courseware-slice-teardown
branch
from
September 17, 2026 01:11
a9f536d to
bdd7fe9
Compare
brian-smith-tcril
marked this pull request as ready for review
September 17, 2026 01:23
6 tasks
arbrandes
force-pushed
the
bsmith/tabpage-outline-query
branch
from
September 18, 2026 14:58
731f3b1 to
504afd8
Compare
arbrandes
force-pushed
the
bsmith/courseware-slice-teardown
branch
from
September 18, 2026 14:58
bdd7fe9 to
a4356bf
Compare
arbrandes
force-pushed
the
bsmith/tabpage-outline-query
branch
from
September 18, 2026 16:13
504afd8 to
f0db910
Compare
arbrandes
force-pushed
the
bsmith/courseware-slice-teardown
branch
from
September 18, 2026 16:13
a4356bf to
5d07ae7
Compare
brian-smith-tcril
force-pushed
the
bsmith/courseware-slice-teardown
branch
from
September 18, 2026 18:05
5d07ae7 to
ba2ba3a
Compare
brian-smith-tcril
force-pushed
the
bsmith/tabpage-outline-query
branch
2 times, most recently
from
September 18, 2026 18:18
9bfbe77 to
1052ae3
Compare
brian-smith-tcril
force-pushed
the
bsmith/courseware-slice-teardown
branch
from
September 18, 2026 18:18
ba2ba3a to
a918db1
Compare
arbrandes
approved these changes
Sep 18, 2026
arbrandes
left a comment
Contributor
There was a problem hiding this comment.
Pre-approved with a minor test concern.
| navigate, | ||
| isPreview: false, | ||
| }); | ||
| const expectedUrl = '/preview/course/courseId/section_1/unit_1'; |
Contributor
There was a problem hiding this comment.
The waitFor calls in these async-path tests are never awaited, so their assertions never run - needs the same treatment as 65dbeb8.
brian-smith-tcril
force-pushed
the
bsmith/tabpage-outline-query
branch
from
September 18, 2026 18:35
1052ae3 to
d2ad1d7
Compare
brian-smith-tcril
force-pushed
the
bsmith/courseware-slice-teardown
branch
from
September 18, 2026 18:35
a918db1 to
7a1eca9
Compare
brian-smith-tcril
force-pushed
the
bsmith/tabpage-outline-query
branch
from
September 18, 2026 18:40
d2ad1d7 to
691fd2e
Compare
brian-smith-tcril
force-pushed
the
bsmith/courseware-slice-teardown
branch
from
September 18, 2026 18:40
7a1eca9 to
b42ec75
Compare
brian-smith-tcril
force-pushed
the
bsmith/tabpage-outline-query
branch
from
September 18, 2026 18:48
691fd2e to
dab2b29
Compare
brian-smith-tcril
force-pushed
the
bsmith/courseware-slice-teardown
branch
from
September 18, 2026 18:48
b42ec75 to
3b47c69
Compare
brian-smith-tcril
force-pushed
the
bsmith/tabpage-outline-query
branch
from
September 18, 2026 19:00
dab2b29 to
2c8cfba
Compare
brian-smith-tcril
force-pushed
the
bsmith/courseware-slice-teardown
branch
from
September 18, 2026 19:00
3b47c69 to
0af5ec5
Compare
brian-smith-tcril
force-pushed
the
bsmith/tabpage-outline-query
branch
from
September 18, 2026 19:16
2c8cfba to
eefbe2f
Compare
brian-smith-tcril
force-pushed
the
bsmith/courseware-slice-teardown
branch
from
September 18, 2026 19:16
0af5ec5 to
84ef0a4
Compare
brian-smith-tcril
force-pushed
the
bsmith/tabpage-outline-query
branch
from
September 18, 2026 19:45
eefbe2f to
73978cf
Compare
brian-smith-tcril
force-pushed
the
bsmith/courseware-slice-teardown
branch
from
September 18, 2026 19:45
84ef0a4 to
395384d
Compare
brian-smith-tcril
force-pushed
the
bsmith/tabpage-outline-query
branch
from
September 18, 2026 19:57
73978cf to
2588130
Compare
brian-smith-tcril
force-pushed
the
bsmith/courseware-slice-teardown
branch
from
September 18, 2026 19:57
395384d to
572be47
Compare
brian-smith-tcril
force-pushed
the
bsmith/tabpage-outline-query
branch
from
September 18, 2026 20:09
2588130 to
c5455e5
Compare
brian-smith-tcril
force-pushed
the
bsmith/courseware-slice-teardown
branch
from
September 18, 2026 20:09
572be47 to
9d60d5f
Compare
brian-smith-tcril
force-pushed
the
bsmith/tabpage-outline-query
branch
from
September 18, 2026 20:17
c5455e5 to
92f1ab2
Compare
brian-smith-tcril
force-pushed
the
bsmith/courseware-slice-teardown
branch
2 times, most recently
from
September 18, 2026 20:34
4854a68 to
9e016fb
Compare
brian-smith-tcril
force-pushed
the
bsmith/tabpage-outline-query
branch
2 times, most recently
from
September 19, 2026 02:58
70430fa to
3aacf76
Compare
brian-smith-tcril
force-pushed
the
bsmith/courseware-slice-teardown
branch
from
September 19, 2026 02:58
9e016fb to
0dbb28e
Compare
brian-smith-tcril
force-pushed
the
bsmith/tabpage-outline-query
branch
from
September 19, 2026 03:12
3aacf76 to
28ebb0f
Compare
brian-smith-tcril
force-pushed
the
bsmith/courseware-slice-teardown
branch
from
September 19, 2026 03:12
0dbb28e to
e45bd00
Compare
The slice's writers were the transitional statusBridge hooks and its readers were CoursewareContainer and TabPage; both convert here, so the bridge, the slice, and the reducer registration are deleted, along with the setupTest seeding, TabPage's transitional string branch (and its slice reads, the sliceError param, the StatusValue type, and the now-orphaned DENIED constant), and the dead Redux-era TabContainer wrapper. The container reads route params and the queries directly: statuses via useIsCourseLoaded and the sequence query, model lookups parameterized by the route ids with the selectors' exact null semantics, and the exported redirect helpers reshaped from positional status-string params to per-helper typed options objects — memoized with shallowEqual so they stay fire-once guards (their bodies unchanged beyond destructuring). The ids-match race guard is deleted with the race it defended against — which also fixes the latent stale-bail where an in-app navigate to /course/:courseId left the old sequence rendered and the resume redirect never ran. Outline policy is the container's own: on outline failure with access granted it renders the home redirect the bridge's denied produced; outline-pending needs no handling (children tolerate a not-yet-loaded outline), so page chrome can render for the tail of the outline fetch where the bridge showed a full-page spinner. TabPage passes through its first typed caller: CourseStatus's metadataQuery is now typed with RequestError, the two-slot object is the whole contract, and deriveView is unary. Closes #1976. Part of #1946. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
brian-smith-tcril
force-pushed
the
bsmith/courseware-slice-teardown
branch
from
September 19, 2026 05:15
e45bd00 to
09a0e12
Compare
brian-smith-tcril
removed this pull request from stack #2062
September 19, 2026 06:44
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.
Summary
The teardown proper — layer B (of six) of #1976 (plan), stacked on the B-prep 403-detail fix #2073. The
coursewareRedux slice, itsstatusBridgewriters, and its reducer registration delete;CoursewareContainerderives everything from route params and queries; the redirect machinery extracts whole tosrc/courseware/redirects.ts(ADR 0008's "liberal courseware path handling") as seven typed, memoized rules behind oneuseCoursewareRedirects()hook; TabPage's transitional string branch retires, leaving the generic two-slot query contract. Closes #1976.What changed
CoursewareContainer.tsxshrinks to its rendering job: route params, the three queries,useCoursewareRedirects(), model reads for the celebration handler, the save-position guard, and theTabPage/Coursetree. The ids-match race guard deletes with the race it defended against (fixing a latent stale-bail: after an in-app navigate to/course/:courseId, the slice kept the stalesequenceIdand the resume redirect never ran).src/courseware/redirects.ts(new): the six redirect rules move out of the container, drop their class-eracheckprefix, and take typed options objects (booleans instead of status strings) memoized withdefaultMemoize(fn, shallowEqual)to keep fire-once guard semantics; the outline-failure bounce (outline failed + access granted → course home) becomes the seventh rule instead of an inline<Navigate>. Their unit tests move toredirects.test.ts.TabPage.tsx: the string branch, the transitionalsliceErrorparam, and both slice reads delete together (as B-prep planned);CourseStatusis the two-slot query object only, now typed withRequestError.courseware/data/slice.js,courseware/data/statusBridge.ts(+ test),tab-page/TabContainer.jsx(+ test, export, stalejest.mock), thecoursewarereducer fromstore.tsand the test store, theStatusValuetype andDENIEDconstant — and the courseHome slice's deadfetchTabFailurereducer, whose sole exercisers were the deleted TabPage string-branch tests (flagged by the zero-tolerance codecov project check; no production dispatcher since refactor: convert the live tab from Redux to React Query #2006).getTestStoreIdshelper insetupTest.jsreplaces ~30 slice-convenience id reads across 19 suites;Sequence.test's load-failure case now states its failure with an explicit 500 mock instead of passing by accident.Testing
npm run types(0 errors),npm run lint(clean), full jest suite green at head (110 suites, 1111 passed / 3 pre-existing skips). Manual pass on tutor local in the details block below (8 of 10 checks, including the full redirect matrix and the stale-bail fix). A running review log was kept alongside the decision doc during pre-submit review of this layer; its durable outcomes are folded into the decision log below.Decisions
Full decision log
Decisions — courseware Redux slice teardown (#1976, layer B)
CoursewareContainerconverts whole. Route params are the id source(
courseId/sequenceId/unitIdstraight fromuseParams); statuses comefrom
useIsCourseLoaded(courseId)and the sequence query(
isSuccess/isError+ thesequenceMightBeUnitpredicate); the fivecreateSelectors become plainuseSelectormodel lookups parameterized bythe route ids, preserving the exact null semantics (
?? null— storedreference or null, stable under default equality;
useModel's{}wouldbreak the helpers'
section &&guards).nextSequenceId/firstSequenceIdderive inline with the same guards, including thefaithful
indexOf === -1quirk of the oldnextSequenceSelector. Onedeliberate leniency: the old
firstSequenceIdSelectorcrashed if asection model was missing while loaded (impossible in practice — the
outline seeds sections); the inline version yields null instead. The
model store is untyped JS until Dissolve the model-store normalized cache #1977, so the three maps this file reads
are named once (
CoursewareModels+readModels) rather thanas any-ingat each read.
The redirect machinery moves to
src/courseware/redirects.ts—review-driven, in three rounds. Round one kept positional params with the
statuses reshaped to booleans; rejected as illegible (6–9 positional args,
now anonymous booleans). Round two gave each rule a typed options object
(a base
RedirectArgsBase+ per-rule extenders; the two section-resolvingrules share
SectionRedirectArgs, withsectionToSequenceRedirectaccepting-but-ignoring
isPreview— that rule has never preview-prefixedits navigations, a pre-existing gap in the A4 jumpnav family, untouched).
Round three extracted the subsystem whole: it is ADR 0008's "liberal
courseware path handling", so the module carries that name and story, the
six rules drop their class-era
checkprefix (resumeRedirect,sectionUnitToUnitRedirect, …) and keep their route-diagram commentsattached, and a self-contained
useCoursewareRedirects()hook owns theno-dep-array effect plus the redirect-only derivations
(
sectionViaSequenceId,firstSequenceId) — the container shrinks to itsrendering job and calls one hook. The stale "move this into the thunks
layer (TNL-8182)" comment dies (the thunks layer no longer exists).
readModels/CoursewareModelslive indata/modelReader.ts, shared bythe container and the hook (both die at Dissolve the model-store normalized cache #1977).
The load-bearing detail: the rules are fire-once guards invoked from a
no-dep-array effect, and
defaultMemoize's per-arg===never matches afresh object literal — a naive object arg would fire the guards every
render (an HTTP GET per render from
resumeRedirect, repeatednavigations). They are therefore memoized as
defaultMemoize(fn, shallowEqual): one-level field-wise===on the argsobject, exactly the comparison depth the positional args had, including
reference equality on the
sequence/sectionmodel objects — correctbecause RTK's immutable updates make reference-change ⇔ content-change
(an optimistic
activeUnitIndexwrite re-fires the sequence redirectrule, as it does today). Constraint for future edits: no args field may
be a per-render fresh literal (an inline array/object would quietly
defeat the guard) — stated once in the module header.
sequencestaysanyin the interfaces — the untyped model reality named byCoursewareModels— because its null-ness is guarded by the siblingisSequenceLoadedboolean, which TS cannot narrow across fields;sectiontypes honestly and narrows via its own&&guard. The rules'tests move to
redirects.test.ts; the TS move surfaced slice-erasequenceId: nulltest values (the runtime is route-paramundefined)and a
URLobject passed to a mock matcher where its.hrefwas meant.The ids-match race guard is deleted with the race. Statuses now derive
from queries keyed by the route ids, so the "redux ids lag the route"
condition it defended against cannot occur. This also fixes the latent
stale-bail: after an in-app navigate to
/course/:courseId(theinvalid-sequence fallbacks), the slice kept the stale
sequenceId, theguard bailed forever, and the resume redirect never ran; with
route-derived ids the redirect checks run and the page self-heals.
Outline policy lives with the redirect rules, not TabPage (the B-prep
decision, landed here; review moved it from an inline container block into
redirects.tsas the seventh rule,outlineFailureRedirect). TabPagereceives the standard two-slot shape — identical to CourseExit's:
{ metadataQuery: courseHomeMetaQuery, tabDataQuery: metadataQuery }. Onoutline failure with access granted, the rule navigates to
/course/:courseId/home— the destination the bridge'sdeniedproducedthrough
getAccessDeniedRedirectUrl's default branch; thehasAccessgate keeps a real access denial's specific redirect (TabPage's) winning.
A 403 outline means no access, which courseHomeMeta also reports, so the
TabPage denied path covers it. Timing: as an effect-time rule (like its
six siblings) rather than a render-time
<Navigate>, the outline-erroredge can paint one frame of chrome before redirecting; the bridge-era
DENIED redirected pre-paint. Outline-pending needs no handling post-A3:
everything under
Coursetolerates a not-yet-loaded outline(
useSequenceIds→[], nav defaults,Sequence's spinner rides theindependent sequence query). Behavior delta: page chrome can render for
the tail of the outline fetch where the bridge showed a full-page spinner.
TabPage's transitional era ends. The string branch, the
sliceErrorparam, the fenced slice-reads block, and the
react-redux/RootState/constants imports all delete together;
deriveViewis unary andCourseStatusis the two-slot object only. ItsmetadataQuerymember isnow typed with
RequestErroras the error type — the container is thefirst typed caller (every tab is
.jsx), which surfaced that thedefault
ErrorTError rejecteduseCourseHomeMeta's actual type.TabContainerdeletes (+ its test, itsindex.jsexport, and a stalejest.mockinsrc/index.test.jsx) — the Redux-era generic tab wrapper(dispatches a
fetchthunk prop, readsstate[slice]string status),renderer-less since the tab conversions. The
index.test.jsxmock datedfrom when
index.jsxwrapped tab routes inTabContaineritself; the tabconversions moved TabPage usage inside the tab components (all mocked in
that suite), so the mock stubbed a module nothing in the tested tree
imports — and with
TabContainergone it would have declaredtab-page'sshape as an export that no longer exists. Found in the pre-B audit as the
only other string-status passer; verified not a plugin surface (internal
module, no docs references). The
StatusValuetype and theDENIEDconstant delete from
constants.tswith their last consumer (TabPage'sstring branch — found in review);
LOADING/LOADED/FAILEDstay withreal consumers outside the retired vocabulary (
course-exit/track.jsusestwo as analytics labels,
preferences-unsubscribeas local componentstate).
The slice infrastructure deletes:
statusBridge.ts(+ its test — thematrix was ported to hook/TabPage tests in A2/A3/B-prep),
slice.js, thecoursewarereducer fromstore.tsand fromsetupTest.js's test store,the
fetchCourseSuccess/fetchSequenceSuccessseeding dispatches (modelseeding stays), the
courseware/data/index.jsreducer export, andCourseExit's
useCourseExitStatusBridgecall (its last two jobs —CourseRecommendations' id, TabPage's message — were retired in A1 and
B-prep).
apiHooks.ts's mutation-vars types move from| null("fromthe still-untyped Redux slice") to
| undefined, includinguseSaveSequencePosition's internalsetPosition: the ids are routeparams now, typed
string | undefined. Deliberately| undefinedratherthan
?— the callers always pass these arguments (possibly holdingundefined), and
?would additionally let a caller omit one entirelywithout a type error.
The courseHome slice's
fetchTabFailurereducer deletes too — flaggedby the codecov project check (this repo's zero-tolerance
threshold: 0%):the reducer's sole exercisers were TabPage's deleted string-branch tests,
which hand-dispatched
course-home/fetchTabFailureto seed the message.In production it has been dead since refactor: convert the live tab from Redux to React Query #2006 deleted the shared
fetchTabthunk, its last dispatcher (the fact B-prep's 403-detail fix rests on);
nothing imports it, nothing dispatches its string type, and it is not
re-exported from
course-home/data/index.js. Same posture as the live-tablayer's orphaned-reducer deletion: remove the dead code the test deletion
orphaned, don't pad coverage. The rest of the courseHome slice (its
now-reader-less
errorMessage/errorCode/courseStatusfields) staysfor Reduce the Redux store to the external specialExams reducer #1978, the slice's own teardown.
Tests.
The redirect-helper unit tests transform positionally to booleans
(
'loaded'→true,'loading'→false,'failed'→truein thefailed slot), with titles renamed to boolean phrasing; the container's
integration suite (real store, bridged client, real routes) passed
unchanged — the strongest evidence the rewrite is faithful.
TabPage's five string-branch cases delete;
mockData.courseStatusisquery-shaped. Removed test → coverage now:
courseStatus="loading"shows the spinnercourseStatus="other"shows the generic errorCourseStatusis the two-slot object), the fall-through-to-error behavior is what the queryisErrorcases assertcourse-home/fetchTabFailuredispatchfetchTabFailuresince the tab conversions; the test kept it alive by hand-dispatching); B-prep'sgetErrorDetailcarries the learner-facing message off the query error insteadcourseware/fetchCourseFailuredispatchSequence.test's load-failure case now mocks an explicit 500 for itssequence URL — it had been passing by accident since A2 (the dead
fetchSequenceFailuredispatch did nothing; the failure came from theunmatched GET hitting
logUnhandledRequests'200 {}fallback, whichthrows in
normalizeSequenceMetadata).Convenience-id sweep: ~15 suites read
state.courseware/getState().coursewarepurely to learn ids; all re-source via agetTestStoreIds(store)helper insetupTest.jsreturning{ courseId, sequenceId }as the first keys of themodels.coursewareMeta/models.sequencesmaps (key order followsoutline insertion, equal to the slice's value for these single-sequence
factories — stated once at the helper instead of implied at every call
site). Process
note for the record: three successive greps under-counted these (a
trailing-dot pattern, a path filter that excluded
src/courseware/'s ownfiles, and a
head-truncated list) — the definitive inventory came fromthe type checker plus the failing suites, which is what a teardown's
compile/test wall is for.
Behavior deltas: the outline-pending chrome tail (4); the stale-bail
fix (3); TabPage surfaces courseware errors as soon as the relevant query
fails instead of after all three settle (transient ordering, same terminal
state); the standard mount-refetch posture for the container's
subscriptions. Otherwise faithful: same redirect rules on the same inputs,
same denied/failed routing, same messages (via B-prep's
getErrorDetail).Closes #1976.
Manual testing
Manual testing — courseware Redux slice teardown (#1976, layer B)
In-browser verification for the teardown, against tutor local
(
http://apps.local.openedx.io:2000/learning, DemoX). This is the plan's fullB smoke matrix: the container now derives everything from route params and
queries, the redirect rules run on booleans, and the
coursewarereducer isgone from the store.
Verify by hand
console errors; Redux DevTools (if open) shows no
coursewareslice./course/{courseId}: lands on thelast-active (or first) unit.
/course/{courseId}/{sectionId}: redirects tothe section's first sequence, then its unit.
/course/{courseId}/{unitId}:spinner, then lands on
/course/{courseId}/{sequenceId}/{unitId}(the 422→ parent-lookup path).
/course/{courseId}/{garbage-id}:bounces to the course root and resumes (this exercises the stale-bail fix —
the old code could hang here after in-app navigation).
/firstand/lastmarkers —/course/{courseId}/{sequenceId}/firstand/lastland on the first/lastunit.
save_unit_positionenabled (or just verify no errors on unit navigation).course-home tabs and courseware redirect per the access rules.
/preview/course/...loads; redirectskeep the
/previewprefix (resume, section, first/last).Results
8 of 10 checks pass against tutor local (DemoX), including the full redirect
matrix (resume, section, unit-as-sequence, invalid-id stale-bail fix,
/first//last, preview prefix). Not exercised: sequence-position save(needs
save_unit_positionenabled on a sequence) and denied learner(needs a restricted-access account/course).
🤖 Generated with Claude Code