Expected Behavior
The courseRun test factory should produce runs whose dates are internally coherent: end_date after start_date, and certificate_available_date at or after end_date. A test that doesn't care about dates should still get a plausible run, so that any test rendering or querying a date range gets a deterministic, sane label.
Current Behavior
Every date field is an independent faker draw, so the ordering between them is random:
|
title: faker.lorem.words(3), |
|
start_date: faker.date.future().toISOString(), |
|
end_date: faker.date.future().toISOString(), |
|
enrollment_start: faker.date.past().toISOString(), |
|
enrollment_end: faker.date.future().toISOString(), |
|
courseware_url: faker.internet.url(), |
|
courseware_id: faker.string.uuid(), |
|
certificate_available_date: faker.date.future().toISOString(), |
start_date: faker.date.future().toISOString(),
end_date: faker.date.future().toISOString(),
enrollment_start: faker.date.past().toISOString(),
enrollment_end: faker.date.future().toISOString(),
certificate_available_date: faker.date.future().toISOString(),
upgrade_deadline: faker.date.future().toISOString(),
start_date and end_date are two independent draws from the same window, so they are ordered correctly only by chance. Measured over 2000 generated runs:
end_date < start_date: 49.3%
certificate_available_date < end_date: 50.1%
enrollment_end < enrollment_start: 0.0%
So roughly half of all generated course runs end before they start, and half advertise a certificate before the run finishes. (enrollment_start/enrollment_end are fine — past() vs future() are ordered by construction.)
Beyond being nonsense data, this actively causes random CI failures. Session options are labelled with the whole range ("Aug 28 - Sep 27, 2026"), so tests that select an option by a regex built from one run's start date can match a second option whenever another run's random end_date lands on that same calendar day:
TestingLibraryElementError: Found multiple elements with the role "option" and name `/Sep 27/i`
<li aria-selected="false" ...>Sep 27 - Nov 3, 2026 (no certificate available)</li>
<li aria-selected="true" ...>Aug 28 - Sep 27, 2026 (no certificate available) — Enrolled</li>
That took down javascript-tests on an unrelated PR: https://github.com/mitodl/mit-learn/actions/runs/30469814580. Because the draw is random, the failure lands on whichever PR happens to be unlucky, and the PR it lands on is never the cause.
Steps to Reproduce
To reproduce the incoherent data directly, add a scratch test anywhere under frontends/main/src and run it with NODE_ENV=test yarn jest <path>:
import { factories } from "api/mitxonline-test-utils"
test("measure factory date incoherence", () => {
const N = 2000
let endBeforeStart = 0
for (let i = 0; i < N; i++) {
const r = factories.courses.courseRun()
if (new Date(r.end_date!) < new Date(r.start_date!)) endBeforeStart++
}
console.log(`end_date < start_date: ${((endBeforeStart / N) * 100).toFixed(1)}%`)
})
To reproduce the resulting flake deterministically, render SessionSelect with one run whose end_date equals the next run's start_date:
const days = (n: number) => new Date(Date.now() + n * 864e5).toISOString()
const a = makeRun({ start_date: days(30), end_date: days(60) })
const b = makeRun({ start_date: days(60), end_date: days(75) })
// ... open the combobox, then:
screen.getByRole("option", { name: new RegExp(formatDate(b.start_date!, "MMM D"), "i") })
// throws: Found multiple elements
Possible Solution
Derive the dependent dates from start_date instead of drawing them independently, so the ordering holds by construction:
const start = faker.date.future()
const end = faker.date.soon({ days: 120, refDate: start })
// ...
start_date: start.toISOString(),
end_date: end.toISOString(),
certificate_available_date: faker.date.soon({ days: 30, refDate: end }).toISOString(),
upgrade_deadline: faker.date.between({ from: start, to: end }).toISOString(),
This doesn't eliminate the label-collision class of flake on its own (two runs could still coincidentally share a boundary date), but it removes the nonsense data and makes collisions far less likely, since ranges no longer overlap arbitrarily.
Additional Details
Scope check before anyone picks this up: courseRun is used across 17 test files and ~230 call sites, of which only ~69 pin end_date explicitly. So ~160 call sites currently receive a random — and about half the time, backwards — date range. Changing the defaults will shift fixture data under all of them, which is why #3697 deliberately fixed only the two affected tests rather than the factory: the blast radius of a factory change is much larger than that flake justified.
That makes this worth doing as its own change, with a full yarn test run to catch any test that was implicitly depending on the current (incoherent) ordering.
Expected Behavior
The
courseRuntest factory should produce runs whose dates are internally coherent:end_dateafterstart_date, andcertificate_available_dateat or afterend_date. A test that doesn't care about dates should still get a plausible run, so that any test rendering or querying a date range gets a deterministic, sane label.Current Behavior
Every date field is an independent
fakerdraw, so the ordering between them is random:mit-learn/frontends/api/src/mitxonline/test-utils/factories/courses.ts
Lines 111 to 118 in 2a66e80
start_dateandend_dateare two independent draws from the same window, so they are ordered correctly only by chance. Measured over 2000 generated runs:So roughly half of all generated course runs end before they start, and half advertise a certificate before the run finishes. (
enrollment_start/enrollment_endare fine —past()vsfuture()are ordered by construction.)Beyond being nonsense data, this actively causes random CI failures. Session options are labelled with the whole range ("Aug 28 - Sep 27, 2026"), so tests that select an option by a regex built from one run's start date can match a second option whenever another run's random
end_datelands on that same calendar day:That took down
javascript-testson an unrelated PR: https://github.com/mitodl/mit-learn/actions/runs/30469814580. Because the draw is random, the failure lands on whichever PR happens to be unlucky, and the PR it lands on is never the cause.Steps to Reproduce
To reproduce the incoherent data directly, add a scratch test anywhere under
frontends/main/srcand run it withNODE_ENV=test yarn jest <path>:To reproduce the resulting flake deterministically, render
SessionSelectwith one run whoseend_dateequals the next run'sstart_date:Possible Solution
Derive the dependent dates from
start_dateinstead of drawing them independently, so the ordering holds by construction:This doesn't eliminate the label-collision class of flake on its own (two runs could still coincidentally share a boundary date), but it removes the nonsense data and makes collisions far less likely, since ranges no longer overlap arbitrarily.
Additional Details
Scope check before anyone picks this up:
courseRunis used across 17 test files and ~230 call sites, of which only ~69 pinend_dateexplicitly. So ~160 call sites currently receive a random — and about half the time, backwards — date range. Changing the defaults will shift fixture data under all of them, which is why #3697 deliberately fixed only the two affected tests rather than the factory: the blast radius of a factory change is much larger than that flake justified.That makes this worth doing as its own change, with a full
yarn testrun to catch any test that was implicitly depending on the current (incoherent) ordering.