refactor!: read units from the sequence query, not useModel - #2128
Merged
Merged
Conversation
brian-smith-tcril
added this pull request to stack #2121
September 25, 2026 20:17
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #2128 +/- ##
==========================================
+ Coverage 94.88% 94.93% +0.04%
==========================================
Files 370 370
Lines 6020 6037 +17
Branches 1425 1427 +2
==========================================
+ Hits 5712 5731 +19
+ Misses 296 294 -2
Partials 12 12 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
brian-smith-tcril
force-pushed
the
bsmith/units-query-reads
branch
from
September 28, 2026 01:55
ff600d7 to
0bc2c6d
Compare
8 of 16 tasks
brian-smith-tcril
force-pushed
the
bsmith/units-query-reads
branch
6 times, most recently
from
September 28, 2026 09:07
e186291 to
d258f97
Compare
Base automatically changed from
bsmith/unit-components-typescript
to
master
September 28, 2026 09:21
Layer D1 of the model-store dissolution (#1977). The six readers of the `units` model read the unit through `useUnit(sequenceId, unitId)`, a disabled `select` observer of the sequence query CoursewareContainer fetches; the bookmark and completion writers patch the cached sequence with `setQueryData` at its exact key; the query no longer writes the `units` model. - `useUnit(sequenceId, unitId)` in `courseware/data/apiHooks.ts`, the `useDiscussionTopic` shape: never fetches, `undefined` for a unit not in the sequence. `sequenceMetadataQuery(sequenceId, isPreview)` is the query's options builder; `useIsPreview()` is the one derivation of the key's preview flag; `updateSequenceUnit(queryClient, key, unitId, patch)` patches one unit of a cached sequence. `SequenceUnit` names the normalizer's fields. - Readers: `Sequence`, `SequenceContent`, `Unit`, `UnitSuspense`, `useShouldDisplayHonorCode`, `UnitButton`. `SequenceContent` threads `sequenceId` to `Unit`, which passes it on. `SequenceContent`'s `!unit` guard is reachable again: a unit id that is not in the sequence renders the no-content message (#98's handling of deleted units, dead since #808 made useModel return `{}` for a missing id). - Writers: `useSetBookmarked(sequenceId, unitId, bookmarked)` gains the sequence id (`BookmarkButton` takes it as a prop, from `unit.sequenceId`); `useCheckBlockCompletion` reads its already-complete guard from the cache; the outline sidebar's completion check names the active sequence, not the clicked unit's, so a cross-sequence click posts to the handler of the sequence that contains the unit. Both hooks drop `useDispatch` / `useStore`. - Bridge: the `units` mirror leaves the sequence query's `meta`; `modelKeys.units` is gone. - Tests: suites that rendered readers against the seeded store mount the container's fetches beside the component and read the cached unit; the two writer suites assert on the cache; `SidebarUnit.test` pins the active sequence in the completion request. Negative check: with `useUnit` fetching, exactly the hook's fetches-nothing case and #2123's request count fail. BREAKING CHANGE: `useModel('units', unitId)` returns `{}`; read the unit with `useUnit(sequenceId, unitId).data` from `courseware/data/apiHooks`, which is `undefined` for a unit not in the sequence. `BookmarkButton` takes a required `sequenceId` prop. `UnitTitleSlot`'s `unit` (the same object in its `pluginProps`) gains `sequenceId`. A URL naming a unit that is not in its sequence renders "There is no content here." instead of an iframe. Part of #1946. Part of #2088. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
brian-smith-tcril
force-pushed
the
bsmith/units-query-reads
branch
from
September 28, 2026 09:21
d258f97 to
56e60cc
Compare
brian-smith-tcril
marked this pull request as ready for review
September 28, 2026 09:40
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 six readers of the
unitsmodel —Sequence,SequenceContent,Unit,UnitSuspense,useShouldDisplayHonorCodeandUnitButton— read the unit through a new hook,useUnit(sequenceId, unitId)incourseware/data/apiHooks.ts, and the sequence query drops itsunitsbridge entry, so nothing writes that model any more. The hook spreads the query's options withenabled: falseand aselectthat finds the unit in the sequence'sunits: it never fetches, becauseCoursewareContainerowns that fetch (#2123), and a disabled observer subscribes to the same cache entry. The two writers that used to dispatch to the store —useSetBookmarkedanduseCheckBlockCompletion— patch the cached sequence withsetQueryDataat its exact key and dropuseDispatch/useStore; the outline sidebar's completion check now names the sequence of the unit being left, which on a cross-sequence click changes where theget_completionrequest goes (see What changed).SequenceContent's!unitguard is reachable again: a URL naming a unit that is not in its sequence renders "There is no content here." — #98's handling of a deleted unit, dead since #808 madeuseModelreturn{}for a missing id. No request-count change:useUnitnever fetches. Breaking for operators — see below. Part of the Redux → React Query migration (#1946, Stage 1); layer D1 of the model-store dissolution (#1977), built on #2123 and #2125. Part of #2088 (layer B closes it).What changed
useUnit(sequenceId, unitId), theuseDiscussionTopicshape (Read discussion topics from the query, not useModel #2087): inlineselect,enabled: falsehard-coded,undefinedfor a unit not in the sequence. Beside it:sequenceMetadataQuery(sequenceId, isPreview)as the query'squeryOptionsbuilder (the suites seed clients through it);useIsPreview()as the one derivation of the key's preview flag, used by the query and both writers;updateSequenceUnit(queryClient, key, unitId, patch)to patch one unit of a cached sequence, a no-op when that sequence is not cached.SequenceUnitnames every field the normalizer produces plus the writer-onlybookmarkedUpdateState(decisions 1, 4, 5).Sequence,SequenceContent,UnitButtonread with their ownsequenceId;SequenceContentthreadssequenceIdtoUnit, which passes it toUnitSuspenseand the honor-code hook — as a prop, not from the route, after the route version crashed every suite that renders the unit subtree without params (decision 2).Unitguards its own read (if (!unit) return nullafter the hooks) so its type is honest without an assertion (decision 11).UnitButtonkeeps its two-termcontentTyperead;UnitIcon'stypeprop becomes optional, which itsdefault:branch already implemented — its TypeScript conversion is Convert the sequence navigation UnitIcon to TypeScript (blocked on mixed FontAwesome majors) #2127, blocked on the repo's mixed FontAwesome majors (decision 6).{}— is replaced by cases for the two states the app reaches; its one unique branch,logEvent's no-units arm, is covered directly now thatlogEvent's body islogSequenceEvent, exported.useSetBookmarked(sequenceId, unitId, bookmarked)gains the sequence id;BookmarkButtontakes it as a prop, fromunit.sequenceId.useCheckBlockCompletionreads its already-complete guard from the cache. The outline sidebar calledcheckBlockCompletion(courseId, sequenceId, activeUnitId)with the clicked row's sequence and the unit being left (that way since [FC-0056] Course outline sidebar #1375); it now passesactiveSequenceId, so a cross-sequence click posts to the handler of the sequence that contains the unit. The one semantic the cache write does not carry over: the store created a record for a unit with no entry; the cache write for an uncached sequence writes nothing.unitsmirror leaves the sequence query'smeta;modelKeys.unitsis gone (decision 7).UnitSuspenseanduseShouldDisplayHonorCodemockuseUnitbeside theiruseModelmock forcoursewareMeta(D3's);SidebarUnit.testpins the sequence the sidebar's completion request names; two directlogSequenceEventcases. Negative check, run: withuseUnitfetching, exactly the hook's fetches nothing on its own and Stop the sequence query refetching from components under the gate #2123's request-count case fail.Operators — breaking
useModel('units', unitId)returns{}: the model is no longer written. Read the unit withuseUnit(sequenceId, unitId).datafrom./src/courseware/data/apiHooks; it isundefinedfor a unit not in the sequence.BookmarkButton(exported from./src/courseware/course/bookmark) takes a requiredsequenceIdprop.UnitTitleSlot'sunit, the same object in itspluginProps, is now the typedSequenceUnitand carriessequenceId; its other fields are unchanged./course/{courseId}/{its own sequence}/{unitId}now renders "There is no content here." at/course/{courseId}/{another sequence with units}/{unitId}, where it used to render under that sequence with navigation that did not match it. Nothing in the app links to that form, and a sequence without units already redirects such a URL to the sequence itself.get_completionrequest for a cross-sequence outline-sidebar click now names the sequence being left, not the one clicked into.Testing
npm run typesandnpm run lintclean; full suite 116 suites, 1187 passed, 0 skipped.git grep "useModel('units'\|modelKeys.units" srcis empty. Manual checks on tutor dev, a minimal pass of the write path with the sidebar request baselined on the layer below: see the checklist.Decisions
Full decision log
Decisions — read units from the sequence query, not
useModel(#2088, layer A)Layer D1 of the #1977 model-store dissolution; the sixth layer of the running
stack #2121, on top of #2124 (#2123), branch
bsmith/units-query-reads.Part of #2088 (layer B closes it). Entries 1, 3–5 and 7 were settled in the
#2088 plan review (2026-09-25, posted to #2088); 2 was revised during
implementation; the rest landed with the code.
useUnit(sequenceId, unitId): a disabledselectobserver of thesequence query.
useQuery({ ...sequenceMetadataQuery(sequenceId, isPreview), enabled: false, select: ({ units }) => units.find(unit => unit.id === unitId) }), theuseDiscussionTopicshape (Read discussion topics from the query, not useModel #2087, entries1–2): inline
select, nouseCallback,undefinedfor a unit not in thesequence, never fetches because
CoursewareContainerowns the fetch(Stop the sequence query refetching from components under the gate #2123). The plan's D1 note against per-model hooks was written for the
one-line
.data ?? {}reads; six copies of afindover the units arrayis what that note was avoiding in the other direction.
sequenceIdreaches the id-only readers as a prop, not from theroute — revised from the posted plan. The plan (A2) had
Unit,UnitSuspenseanduseShouldDisplayHonorCodereaduseParams().sequenceId,on the precedent of
UnitButtonand the navigation hooks. Implemented thatway, every suite that renders the unit subtree without route params
(
Course.test,SequenceContent.test, the Sequence suite's navigationcases) crashed on
unit.titleof an undefined unit: the router couplingput a production invariant (there is always a
:sequenceId) onto suitesthat had never needed one.
SequenceContentnow passessequenceIdtoUnit, which passes it toUnitSuspenseand the hook's argument object;three propTypes entries, no router reads.
UnitButtonkeepsuseParams(), which it already used for its link.undefinedwhere{}was;SequenceContent's!unitguard wakesup. Every reader takes an optional read (
unit?.title,?? {}on theone destructure that stays). The guard's history, from the plan review:
Show message when there are no units in a sequence. #60 (2020-05) returned the message on
unitId === null; [BD-29] [TNL-7288] Fix front-end behavior when the course has no sections or no subsections in the first section #98 (2020-07,TNL-7288) changed it to
!unitId || !unitto handle deleted units, whenuseModelreturnedundefinedfor a missing id; fix: re-enable access error redirects for course home #570 (2021-07) made amissing type return
{}; fix: [AA-1018] api refactor #808 (2022-02, AA-1018) extended{}to amissing id to fix a course-home-to-courseware error, its author noting
"there was code that depended on each behavior", and patched the one
dependent a test caught (
UnitNavigationEffortEstimate, layer B'sObject.keysguards);SequenceContentbroke no test and went dead. AURL naming a unit that is not in the sequence now renders "There is no
content here." instead of an iframe the LMS then fails to load. Pinned by
displays the no-content message for a unit that is not in the sequence
in
Sequence.test.jsx.One existing case tested a state the app cannot enter. handles the
navigation buttons for empty sequence (TNL-7268, 2020-06) rendered
Sequencefor a sequence with no units together with a unit id fromanother sequence, and clicked previous/next to reach the handlers'
empty-
unitIdsbranches andlogEvent's: 0arm. It passed on the storebecause
useModel('units', id)looked the unit up by id, with no regardfor which sequence was on screen;
useUnitlooks inside the namedsequence's entry and finds nothing, and the awakened guard renders the
no-content message. Checked by hand on the layer below (2026-09-25): the
app never reaches that state anyway —
sequenceUnitMarkerToSequenceUnitRedirect'sno-units branch (
redirects.ts:216) sends/course/{c}/{seq}/{anything}to
/course/{c}/{seq}for a sequence with no units, soSequenceonlyever sees an empty sequence with no unit id, which renders the no-content
message and no navigation on every layer. The case is replaced by shows
the no-content message for a sequence with no units, the state the app
does enter. Measured with the original case removed: the only coverage it
uniquely held was the
: 0arm oflogEvent'scurrentIndex(atracking-payload adjustment for a sequence with no units), which an
operator's
SequenceNavigationSlotoverride can still reach.logEvent'sbody is lifted out of the component as
logSequenceEvent(eventName, { sequence, unitId, widgetPlacement, targetUnitId }), exported, with thein-component
logEventa one-line wrapper so the call sites areunchanged, and two direct cases cover the payload, the no-units arm
included (settled in review, over stubbing the slot or filling it with a
real plugin). The handlers' first/last-unit branches stay covered by the
neighbouring cases. Not covered here: a unit id that is not in a
non-empty sequence, which no redirect rule catches — on
masterthesentinel rendered that unit under the wrong sequence, on this layer it is
the no-content message; reachable only by editing the address bar.
Types.
SequenceUnitnames every fieldnormalizeSequenceMetadataproduces — the normalizer is ours, so no index signature — plus
bookmarkedUpdateState?: 'loading' | 'loaded' | 'failed', the one fieldthe endpoint never sends and
useSetBookmarkedwrites.SequenceMetadatais the
CoursewareMeta-style placeholder (index signature, comment naminglayer B).
SequenceMetadataDatais the query result.sequenceMetadataQuery( sequenceId, isPreview)is aqueryOptionsexport likediscussionTopicsQuery, whichuseSequenceMetadataanduseUnitspreadand the suites use to seed a client through the real query
(
queryClient.fetchQuery(sequenceMetadataQuery(id, false))) rather than ahand-written shape (Stop the courseware gate queries refetching from components under the gate #2098, entry 3).
Writers address the exact cache entry; one key derivation. The
sequence key carries the route's preview flag, so
useIsPreview()isexported beside the query and every hook that reads or writes the key
takes the flag from it; no writer reads the route on its own.
updateSequenceUnit(queryClient, queryKey, unitId, patch)patches oneunit of a cached sequence and is a no-op when that sequence is not cached
(the updater returns
undefined). Consequences:useSetBookmarked(sequenceId, unitId, bookmarked)gains the sequenceid;
BookmarkButtontakes asequenceIdprop, whichUnitTitleSlotpasses from
unit.sequenceId, and the slot'sunitshape names it.useCheckBlockCompletion's guard reads the cached unit'scomplete;the sidebar's call passes
activeSequenceId(from the route) with theactive unit instead of the clicked unit's sequence (
course-outline/ hooks.js:80, that way since the sidebar was created in [FC-0056] Course outline sidebar #1375). On across-sequence click the
get_completionrequest now goes to thesequence that contains the unit.
handleUnitClickdrops its unusedsequenceIdandUnitLinkWrapperstops passing it. Pinned by sends thecompletion check for the unit navigated away from to its own sequence,
not the one navigated to in
SidebarUnit.test.jsx.updatecreated a record for a unit with no entry; the cache write fora sequence that is not cached (or a unit not in it) writes nothing.
Nothing reads a unit that has not been fetched. Pinned by proceeds to
the request for a sequence with no cache entry, and writes nothing and
writes nothing for a sequence with no cache entry (both renamed in
layer B's review from … when the sequence is not cached / … that is
not cached, amended here so the names stay with the layer that wrote
them).
Rejected in the plan review: addressing by unit id across every cached
sequence entry (
setQueriesDataover a prefix), which reproduced thestore's lookup but wrote by scanning entries the cache keys by request,
and existed mainly to keep the sidebar's mis-keyed call out of the diff.
UnitIcon'stypebecomes optional, in JavaScript;UnitButtonpasses
contentTypethrough as it is. WithuseModel'sanygone,TypeScript sees
contentTypeundefined when the unit is not loaded and nofallback prop was passed, against
UnitIcon'spropTypes.isRequired.That state already reached
UnitIcon'sdefault:branch onmaster; thepropTypeswere stricter than the component, sotypeis now optionaland
UnitButtonkeeps the two-term read,unit?.contentType ?? fallbackContentType, the destructuring default it replaced. Convertingthe file instead, as Convert Unit, UnitSuspense, UnitTitleSlot and BookmarkButton to TypeScript #2125 did for the four components this layer edits,
was tried and backed out: the one line that renders the icon fails
npm run types(IconDefinitionis not assignable toIconProp) becausethe repo mixes FontAwesome majors —
free-solid-svg-icons5.15.4 with itsnested
common-types0.2.x against the hoistedfontawesome-svg-core/common-types6.7.2 — and no.tsxfile passes an icon toFontAwesomeIconyet. The conversion is Convert the sequence navigation UnitIcon to TypeScript (blocked on mixed FontAwesome majors) #2127, gated on aligning thosemajors. A first version substituted
?? 'other'inUnitButton, thecase whose icon matches the default branch; rejected in review as an
invented value standing in for a type that should have allowed its
absence.
Bridge: the
unitsmirror leavessequenceMetadataQuery'smeta;modelKeys.unitsgoes fromUnit/constants.ts(coursewareMetastaysfor D3).
seedSequenceModelskeeps seedingunitsinto the store untilF, harmlessly;
getTestStoreIdsstill readsmodels.sequences.Suites read the client the component renders under, seeded through the
real query.
UnitButton,SequenceContent,Unit/index,SequenceNavigationDropdownmountMountCourseQueryHookswith thesequence id beside the component, at a route with
:sequenceIdwhere thecomponent reads the route (Stop the sequence query refetching from components under the gate #2123's helper).
Unit/index.testrendersUnitbeside the owner and awaits its output:Unitreturnsnulluntil
useUnithas the unit (see the note below), so no test-side gateis needed.
BookmarkButton.testnests aQueryClientProviderfor a client it seeds (Read discussion topics from the query, not useModel #2087, entry 3) and assertson the cached unit in place of
store.getState().models.units; the twowriter suites do the same with a
MemoryRouterforuseIsPreview. Thebookmark hook suite seeds a minimal entry by hand,
{ sequence: {}, units: [{ id, bookmarked: false }] }, rather than afactory-built sequence through the real query — settled in review: the
hook touches only
units[].idand the patched fields, and the factorysetup was more than that dependency warranted. The completion suite keeps
the real query, since its outline roll-up reads the real unit ids.
UnitSuspense.testanduseShouldDisplayHonorCode.testmockuseUnit(
{ data }) beside theiruseModelmock forcoursewareMeta, which D3removes.
useShouldDisplayHonorCodewith no unit yieldsundefined, and thenew case says so.
setShouldDisplay(userNeedsIntegritySignature && graded)withgradedundefined storesundefined, exactly as it didwith
useModel's{}; does not display while the unit is not loadedasserts
toBeUndefined()with a comment, rather thanfalse, which thehook does not produce there.
Commit:
refactor!:with aBREAKING CHANGE:footer. Nolearner-visible change except decision 3's, and no request-count change
(
useUnitnever fetches; the count is Stop the sequence query refetching from components under the gate #2123's). For plugins:useModel('units', unitId)returns{}, the replacement isuseUnit(sequenceId, unitId).data(undefinedfor a unit not in thesequence);
BookmarkButton, exported fromcourseware/course/bookmark,takes a required
sequenceIdprop;UnitTitleSlot'sunitprop gainssequenceId, and itspluginProps.unitis the same object.Negative check, run: with
useUnit'senabled: falseflipped totrue,exactly two cases fail — the hook's fetches nothing on its own and #2123's
requests the sequence metadata once per load (the six readers refetching
on mount, 1 request becoming 2); the sequence and navigation suites still
pass. Full suite on this layer: 116 suites, 1183 tests.
Unitguards its own read:if (!unit) { return null; }after thehooks. Under
SequenceContentthe unit is always there — its!unitbranch renders the no-content message instead of
Unit— butuseUnitcannot promise that to TypeScript, and
Unithands the whole unit toUnitTitleSlot. Three shapes were weighed in review: a non-nullassertion (
useUnit(...).data!, what the rebase onto refactor: convert Unit, UnitSuspense, UnitTitleSlot and BookmarkButton to TypeScript #2126 firstproduced — correct at runtime, invisible in review, and enforcing nothing
if
Unitis ever rendered elsewhere); passing the unit down fromSequenceContentas a prop (rejected:Unittaking aunitprop readsoddly, and threading a value a reader can supply sets a prop-drilling
pattern); and the guard (chosen: a cheap restatement of the parent's
check that makes
Unitcorrect on its own). Thenullrender is a stateproduction does not reach.
Manual testing
Checklist
Manual testing — read units from the sequence query, not
useModel(#2088, layer A)In-browser verification against a live backend (tutor dev).
What changed: the six readers of the
unitsmodel (Sequence,SequenceContent,Unit,UnitSuspense,useShouldDisplayHonorCode,UnitButton) read the unit throughuseUnit(sequenceId, unitId), a disabledselectobserver of the sequence queryCoursewareContainerfetches; thebookmark and completion writers patch the cached sequence with
setQueryDataat its exact key instead of dispatching to the store; the sidebar's completion
check names the active sequence; the query no longer writes the
unitsmodel. No request-count change intended:
useUnitnever fetches.The bugs this layer could introduce. (1) A reader that never sees the
unit — a disabled observer with nothing fetching its query stays
pendingforever, so the unit title, bookmark button, completion ticks and the
honor-code / content-gating gates would never appear, with no error. Every
reader renders under
CoursewareContainer, which fetches. (2) A write thatlands nowhere — a writer building a different key from the reader's
(preview flag, sequence id) patches nothing, so a bookmark click or a
completion check would leave the UI unchanged until the next sequence fetch.
(3) Stale reads after a write — if the cache write did not produce a new
object, observers would not re-render (the #2086 entry-11 concern). (4)
undefinedwhere{}was — a unit id that is not in the sequence nowrenders the no-content message (decision 3), and an operator's
UnitTitleSlotwidget reading
unit.…sees the same object as before, now withsequenceId. (5) The cross-sequence completion request — the sidebar'sget_completionPOST now goes to the active unit's sequence handler.Setup
A course with two or more sequences of several units, with completion
tracking on. A graded unit in a course with the honor code / integrity
signature enabled, if available. A unit with content-type-gated content, if
available. Preview mode available (staff). Devtools Network filtered to
courseware/sequence|get_completion|bookmarks, Console open.Checks
Request count (bug 1, and no new observers) — the #2098 protocol
api/courseware/sequence/{id}1 (unchanged from Stop the sequence query refetching from components under the gate #2123).courseware/sequencerequest./preview/course/…; onecourseware/sequencerequest withpreview=1; the unit title and bookmark button render.Bookmarks (bugs 2 and 3)
api/bookmarks): click bookmark; the button reverts to un-bookmarked and the console logs the error./preview/…; the button and tab update (the write lands on the preview-keyed entry).Completion (bugs 2, 3 and 5)
get_completionPOST goes toxblock/{current sequence}/handler/get_completionwith the unit you left; its tab shows the tick.get_completionrequest.Gates (bug 1)
The awakened guard (bug 4)
/course/{id}/{sequenceId}/{unit id from another sequence}for a sequence that has units: "There is no content here." renders, no iframe, no unit navigation. Baseline on the layer below (run 2026-09-25): the other sequence's unit rendered under this sequence, previous/next did not go to the right places, and the outline sidebar showed the sequence expanded with no unit highlighted./course/{id}/{emptySequenceId}/{any unit id}: the redirect strips the unit id and the page shows "There is no content here." with no navigation. Baseline on the layer below (run 2026-09-25): the same — the no-units redirect predates this layer.useUnit,updateSequenceUnit,useIsPrevieworsequenceMetadataQuery.Results
Run 2026-09-25 on tutor dev, a minimal pass of the write-path checks; the
rest skipped (see Not run). 5 of 16 checks run, all passing.
Run
during the request, the tab icon appeared and disappeared.
/preview/…updated the button andthe tab (the write reached the preview-keyed entry).
get_completionPOST, to the sequencenavigated from, for the unit navigated from. Baseline on the layer below,
same click: one POST, to the sequence navigated to — the mis-keyed call
decision 5 corrects.
get_completionrequest.Observed, outside this layer
get_completionPOSTs, both to the sequence navigated from, on this layerand on the layer below alike. That path is
Sequence→unitNavigationHandler→
CoursewareContainer.handleUnitNavigationClick, which this layer doesnot touch; the second request's initiator and payload were not captured.
Pre-existing; not chased here.
Not run
Stop the sequence query refetching from components under the gate #2123;
useUnitnever fetches, pinned by fetches nothing on its own).the bookmark hook suite).
writer as the cross-sequence check, with no key mismatch to correct).
the
useShouldDisplayHonorCodeandUnitSuspensesuites cover the gatesagainst a mocked
useUnit).the review — see the checklist lines — and pinned by displays the
no-content message for a unit that is not in the sequence and shows the
no-content message for a sequence with no units).
🤖 Generated with Claude Code