Skip to content

test(e2e): a fixture the seeder owes must FAIL when it is missing - #613

Merged
rubenvdlinde merged 12 commits into
developmentfrom
fix/e2e-a-missing-fixture-must-fail
Aug 27, 2026
Merged

test(e2e): a fixture the seeder owes must FAIL when it is missing#613
rubenvdlinde merged 12 commits into
developmentfrom
fix/e2e-a-missing-fixture-must-fail

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

test.skip(!row, 'No X seeded in this environment.') answers two different
questions the same way, and reports the second as a pass:

the instance… the right answer
was never seeded the question cannot be asked — skip
was seeded and X is absent the seeder owes it — that is a defect, and it must fail

What the conflation already cost

#608 fixed one instance after it swallowed an entire scenario. The
talk-classroom third test ran zero times on every green run:
Enrolment.lifecycle defaults to 'pending', the seeder never set 'active',
so a lifecycle === 'active' guard matched nothing forever. Earlier still the
lookup ended in ?? rows[0], so a miss asserted against an arbitrary row
while still looking green.

That fix was local to one spec. Twenty more of the same guard sat in five
other files, each able to fail the same way and none able to say so:

spec guards
report-card 10
course-authoring-ux 4
adaptive-release 3
progress-tracking 2
groepsplan 1

All 23 are now requireFixture().

Why the helper moved

Three specs had already grown their own copy of the seeded check. A rule about
what counts as covered should not be spelled three ways, so it lives in
tests/e2e/seeded.ts now. It reads the seeder's marker file
(.e2e-state/seeded-schemas.json), never process.env — globalSetup mutates
the runner's environment and test workers are separate processes, so the
variable it set is not there to be read.

⚠️ This is meant to surface work

Any scenario that now fails was already not covering anything — the failure
is the first honest report of that, not a regression this PR introduces. The fix
for one is to seed the fixture, or to fix why the seeder does not produce it.
Never to put the skip back.

I would rather this PR go red and tell the truth than stay green by not asking.

Verification

prettier --check clean on every touched file (the one reformat is my own new
helper, and it is whitespace only). No root tsconfig.json, so there is no
typecheck to run. No assertion deleted or weakened, and no new skip added.

`test.skip(!row, 'No X seeded in this environment.')` answers two different
questions the same way, and reports the second as a pass:

  - the instance was never seeded  -> the question cannot be asked; skip
  - the instance WAS seeded and X is absent -> the seeder owes it; that is a
    DEFECT, and it must fail

#608 fixed one instance of this after it cost an entire scenario: the
talk-classroom third test ran ZERO times on every green run, because
`Enrolment.lifecycle` defaults to 'pending', the seeder never set 'active', and
the guard matched nothing forever. Earlier still, the lookup ended in
`?? rows[0]`, so a miss asserted against an ARBITRARY row while looking green.

That fix was local to one spec. Twenty more of the same guard were left across
five other files, each able to fail the same way and none able to say so.

  adaptive-release      3
  course-authoring-ux   4
  groepsplan            1
  progress-tracking     2
  report-card          10

All 23 raw guards are now `requireFixture()`. The helper moves to
`tests/e2e/seeded.ts` because THREE specs had already grown their own copy of
the seeded check, and a rule about what counts as covered should not be spelled
three ways.

⚠️ This is expected to surface work rather than stay quiet. Any scenario that
now fails was already not covering anything; the failure is the first honest
report of that. The fix for one is to seed the fixture, or to fix why the seeder
does not produce it — never to put the skip back.

Verified: prettier clean on every touched file, no root tsconfig so no
typecheck to run, and no assertion deleted or weakened.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/learniq @ d8bb824

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-specs
format
check-l10n-js
check-schema-l10n
composer ✅ 104/104
npm ✅ 650/650
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman ⏭️
Playwright
Hydra gates

Quality workflow — 2026-08-25 22:57 UTC

Download the full PDF report from the workflow artifacts.

Conduction Release Bot added 3 commits August 26, 2026 10:55
Converting the skip guards turned fourteen scenarios red, and every one named
a fixture that simply is not there. Nine of them were not "missing a row" but
missing a SCHEMA: this seeder created zero report-period, report-card,
group-plan and group-plan-subgroup objects, so four whole surfaces had never
been exercised by anything.

Added:

  Lesson    published+ungated text, cmi5, one gated on a prerequisite,
            and one drip-released (availableAfterDays)
  ReportPeriod  one locked-and-open, one composed, both scoped to the demo
            CurriculumPlan and Cohort
  GradeEntry    a concept entry inside the locked period's scope, and a
            published entry whose visibility window has not opened
  ReportCard    rapportvergadering-review (empty mentorComment), finalised
            (citing that not-yet-visible entry), and published-to-parents
  GroupPlan + an intensief subgroup that actually has members

Three things worth stating, because each is a way this could have gone quietly
wrong:

`isLocked` is NOT a stored field. The register declares it as a materialised
calculation — `lockDate` set AND `lockDate < now` — which is what
ReportPeriodComposeGuard reads. A period is therefore made locked by giving it
a lockDate in the PAST; writing `isLocked` would have set a property the schema
does not have, and OR would have been right to ignore it.

`releaseConditions[].lessonId` carries a `$ref`, and OR refuses an explicit
null on a $ref-bearing property while accepting an absent key — the same
distinction that made lesson blocks fail to save. The condition is only added
once there is a real lesson to point at.

The not-yet-visible GradeEntry uses a date far enough out that the fixture
cannot quietly expire. A fixture that stops matching on some future date
reopens exactly the hole this change closes.

Verified: every field checked against the register — no unknown properties, and
no required field left unsent. `node --check` passes. `.mjs` is outside this
repo's prettier glob, so its formatting is left alone deliberately.
…mpute it

Three defects, all of them found by the same red run — the one the skip
conversion produced.

1. THE COMPOSE BUTTON COULD NEVER BE CLICKED.

`ComposeReportPeriodModal` gated on `period.isLocked === true`. `isLocked` is a
declared x-openregister-calculations entry with `materialise: true`, and it IS
computed — a create response carries `isLocked: true` for a past lockDate.
It is simply never returned again. Measured against a live instance:

    POST  (create response)  -> isLocked: true
    GET   /objects/.../{id}  -> field ABSENT
    GET   /objects/...?_limit -> field ABSENT

So the value existed for exactly one HTTP response and was unreadable
thereafter. `period.isLocked` was `undefined` on every fetch the dialog makes,
`=== true` was always false, and Compose stayed disabled no matter how long ago
the period locked. ReportPeriodComposeGuard survived only because it already
carries a defensive fallback — and its docblock says why, which means someone
had seen this shape before. Both sides now agree: prefer the materialised
value, else compute `lockDate` set AND in the past.

2. THE SUBGROUP LIST ADDRESSED A SCHEMA NAME, NOT A SLUG.

`/objects/learniq/GroupPlanSubgroup` — OpenRegister resolves by SLUG
(`group-plan-subgroup`). Slugs are case-insensitive, but case-insensitivity does
not insert hyphens, so it resolved to nothing and answered HTTP 200 with an
empty result set: indistinguishable from "the seeder made none". Every other
list constant in the suite already used the slug form.

3. TWO SEEDS WERE REFUSED BY A $ref LIMITATION.

`403 Unresolved reference: schema:///Lesson#` and
`schema:///CurriculumPlan#` — OpenRegister does not resolve a $ref that sits
inside an ARRAY ITEM, even when the target object exists. The gated Lesson can
drop it (`releaseConditions.items.required` is `["kind"]` alone). The finalised
ReportCard cannot: `subjectGrades.items.required` is `["curriculumPlanId"]`. So
the finalised card is seeded without subjectGrades, which serves the
finalised-card scenario, and the narrower "finalised card citing a not-yet-
visible GradeEntry" scenario is LEFT FAILING rather than skipped. A red test
naming a real platform limitation is worth more than a green one asserting
nothing.

Measured before this commit: 14 failed -> 9 failed, 437 -> 442 passed. Of those
9, three had already stopped being fixture problems and become real assertion
failures — which is the whole point.
Both asked only for contentType + published, which was unambiguous while
exactly one such Lesson existed. The seeder now also creates a release-gated
and a drip-delayed text Lesson, and LessonPlayer renders its LOCKED branch for
those — a branch with no footer, and so no 'Mark lesson complete' button.

For the text scenario that meant the assertion turned on whichever row the API
happened to return first.

The cmi5 scenario is worse, because it asserts the button is ABSENT: a gated
lesson shows no button either, so selecting one would have made it pass for
entirely the wrong reason and prove nothing about contentType at all.

Both now require the ungated shape adaptive-release.spec.ts already uses.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/learniq @ 58d3f49

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-specs
format
check-l10n-js
check-schema-l10n
composer ✅ 104/104
npm ✅ 650/650
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman ⏭️
Playwright
Hydra gates

Quality workflow — 2026-08-26 10:35 UTC

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/learniq @ 3431155

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-specs
format
check-l10n-js
check-schema-l10n
composer ✅ 104/104
npm ✅ 650/650
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman ⏭️
Playwright
Hydra gates

Quality workflow — 2026-08-26 11:42 UTC

Download the full PDF report from the workflow artifacts.

Conduction Release Bot added 3 commits August 26, 2026 14:56
gate-16 is diff-scoped, so rewriting isLocked() made it a changed method that
must carry an @SPEC. Its siblings in this file all point at
openspec/changes/report-card-composer/... — a path that no longer exists, since
that change was archived. Those tags are dead references that pass the gate
only because gate-16 checks presence, not resolution.

This one cites the LIVE spec instead: openspec/specs/report-card/spec.md, whose
requirement defines locked as 'lockDate is set AND has passed @now' and mandates
that the GUARDS read isLocked directly. It says nothing about this dialog, which
is exactly why reading the same field here with no fallback went unnoticed.
…ister#2179

This scenario needs a finalised ReportCard whose subjectGrades cite the
not-yet-visible GradeEntry. That object cannot be written through the object API
at all: OpenRegister refuses a $ref inside an ARRAY ITEM with
'403 Unresolved reference: schema:///CurriculumPlan#', and
subjectGrades.items.required is ["curriculumPlanId"] — the refused property is
the required one, so there is no valid object to fall back to.

openregister#2179 already tracked this from hermiq. I added learniq's three
instances to it: Lesson.releaseConditions[].lessonId (self-referential),
ReportCard.subjectGrades[].curriculumPlanId, Assessment.itemRefs[].itemId — the
last of which is why this seeder creates zero assessments.

fixme, not skip: it reports as a KNOWN failure rather than as a pass, and it
names an issue someone can close. Declared as test.fixme(name, fn) rather than a
bare test.fixme(true, …) statement, which would have applied to every test in
the describe and quietly taken the siblings with it.
Same root cause as the report-card scenario, but it hides better, because the
fixture LOOKS present.

The scenario needs a Lesson whose releaseConditions carries a lesson-completed
entry POINTING AT a prerequisite. `lessonId` is a $ref inside an array item, so
OpenRegister refuses the write with 'Unresolved reference: schema:///Lesson#'
(ConductionNL/openregister#2179) — self-referential, refused all the same.

Dropping `lessonId` makes the write succeed, because items.required is ['kind']
alone. That is the trap. LessonReleaseEvaluator opens with:

    $lessonId = (string)($condition['lessonId'] ?? '');
    if ($lessonId === '') { return ['blocked' => false, …]; }

so a condition without a lessonId is treated as NOT BLOCKING. The row satisfies
the discovery predicate while being unable to lock anything — the page rendered
normally and the test failed on toContain('not available') against an entirely
ordinary lesson.

A toothless stand-in is worse than no fixture: it would let a green run claim
coverage of locking. fixme until #2179 lands, and the seeder now says outright
that this row is not a working gate.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/learniq @ e1b2e1f

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-specs
format
check-l10n-js
check-schema-l10n
composer ✅ 104/104
npm ✅ 650/650
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman ⏭️
Playwright
Hydra gates

Quality workflow — 2026-08-26 14:10 UTC

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/learniq @ 14b60bd

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-specs
format
check-l10n-js
check-schema-l10n
composer ✅ 104/104
npm ✅ 650/650
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman ⏭️
Playwright
Hydra gates

Quality workflow — 2026-08-26 14:58 UTC

Download the full PDF report from the workflow artifacts.

Conduction Release Bot added 2 commits August 26, 2026 22:37
gate-96 (manifest-copy-style) has been failing on learniq's development, not
just on this branch — I checked development's own gates job before assuming it
was mine. voice.md §8 bans em-dashes and double-dashes in user-visible manifest
copy; this was the one string breaking it:

  progress.json pages[3].config.widgets[2].content.emptyText
  'No sub-competencies found — this is a leaf node (learning outcome).'

Replaced the em-dash with a period, which is what the gate's own guidance says
to do. Verified by running the gate against the tree: 1274 manifest strings
checked, zero findings. No l10n catalogue carried the old string, so nothing
goes stale.
The two review scenarios found their card, navigated to the review page, and
found no rows — 'element(s) not found' on Finalise and Reopen.

RapportvergaderingReviewView renders the grid inside a v-else:

    v-if="period.lifecycle === 'open'"  -> 'not composed yet' + Compose button
    <template v-else>                    -> the grid, with Finalise / Reopen

so a card hanging off an OPEN period is invisible however correct the card
itself is. The card list query is NOT lifecycle-filtered, which is what made
this look like a data problem: I checked the query, saw no gate, and moved on.
The gate is in the template.

It is also what the domain says. Cards come into existence BY composition, so a
card on a period that was never composed is not a state the app can reach — the
fixture was describing an impossible world.

All three cards now hang off P2 (composed). P1 stays exactly as it was: two
scenarios discover it by shape (open + isLocked, and the lock-guard one that
matches a concept GradeEntry on its periodCode), so it is load-bearing even
though nothing references the binding — noted in place so it does not get
tidied away.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/learniq @ 43f2332

Check PHP Vue Security License Tests
lint ⏭️
phpcs ⏭️
phpmd ⏭️
psalm ⏭️
phpstan ⏭️
phpmetrics ⏭️
eslint ⏭️
stylelint ⏭️
build ⏭️
composer ⏭️ ⏭️
npm ⏭️ ⏭️
app:check-code ⏭️
info.xml ⏭️
REUSE ⏭️
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-08-26 20:44 UTC

Download the full PDF report from the workflow artifacts.

… the dashboard

Five of the six remaining failures had one cause, and it was not the fixtures.

src/main.js builds the router with
`createWebHistory(generateUrl('/apps/learniq'))`. vue-router strips that base
from `location.pathname` and appends the UNTOUCHED hash, so

    /index.php/apps/learniq/#/report-periods/<id>/review

resolved to `/#/report-periods/<id>/review`, matched no declared route, and
fell through `routesFromManifest`'s `/:pathMatch(.*)*` catch-all — which
`redirect: '/'`s to the DASHBOARD.

So every assertion in these scenarios was made against the dashboard. That is
why they reported `element(s) not found` for Finalise, Reopen and
"Compose report cards…", and why two of them matched their expected text
against a page beginning "Skip to app navigation … Learniq".

⚠️ THIS WAS INVISIBLE PRECISELY BECAUSE THEY SKIPPED. A scenario that stands
down for want of a fixture never navigates, so a broken URL costs nothing and
shows nothing. Seeding the fixtures made them navigate for the first time, and
they landed in the wrong place immediately.

The repo already knew: talk-classroom-spaces, course-authoring-ux,
detail-pages, index-pages and accessibility-conformance each carry this warning
in prose, added when it was fixed there. These four had simply not been reached.

⚠️ STILL OUTSTANDING, DELIBERATELY OUT OF SCOPE HERE: twelve other spec files
use the same hash form and currently PASS — while asserting against the
dashboard. That is the same hollow green this branch exists to remove, and
fixing them will turn several red for real reasons. It wants its own change
rather than being folded into this one.

Verified: prettier clean across the suite, eslint 0 errors.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/learniq @ d4daf94

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-specs
format
check-l10n-js
check-schema-l10n
composer ✅ 104/104
npm ✅ 650/650
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman ⏭️
Playwright
Hydra gates

Quality workflow — 2026-08-26 22:26 UTC

Download the full PDF report from the workflow artifacts.

Conduction Release Bot and others added 2 commits August 27, 2026 00:40
⚠️ THE PR WAS CONFLICTING, AND ITS CHECKS WENT GREEN ANYWAY. Five checks
passed where this branch normally runs 37 — a conflicted PR runs no real CI, so
that green described nothing. The tell is the check COUNT, not the colour.

Two of my changes had already been made on development while this branch was in
flight, which is what conflicted:

  #610 navigate by path, not by hash — and it covers NINETEEN spec files, where
       I had fixed the four this branch made run. Theirs is the better change;
       I kept only the comments recording what the bug cost in these specs.

  #623 rewrote the same em-dash empty state gate-96 flagged, with better copy
       ("No sub-competencies. This is a learning outcome, the lowest level in
       the tree."). Took development's wording wholesale.

Everything unique to this branch survives the merge, verified rather than
assumed: seeded.ts, 26 requireFixture call sites, both named test.fixme
declarations, the isLockedPeriod helper, the seeder's fixture block, and the
`group-plan-subgroup` slug fix.

Verified on the merged tree: seeder parses, prettier clean across the suite,
eslint 0 errors, manifest is valid JSON, and gate-96 reports 1274 strings
checked with no findings.
@rubenvdlinde
rubenvdlinde merged commit c41e516 into development Aug 27, 2026
82 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/learniq @ 39436ae

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-specs
format
check-l10n-js
check-schema-l10n
composer ✅ 104/104
npm ✅ 650/650
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman ⏭️
Playwright
Hydra gates

Quality workflow — 2026-08-27 01:23 UTC

Download the full PDF report from the workflow artifacts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant