Integration test — the three axes narrow independently, and an unconstrained lesson still reaches every query
The story-level test, against the real Postgres the other lesson suites use. It proves the three axes end to end and pins the behaviour whose loss would be invisible.
⚠️ AMENDED 2026-08-21, mid-run, ON THE RECORD — three of the five expected sets below were wrong (planning bug MOTIR-3355). The original table listed assertion 2 as "C and D", assertion 3 as "E and D" and assertion 5 as "E returns, and so does D". Each of those omits the rows that are unconstrained on the axis being queried — E on
types, C onphases, A/B/C onkinds— which are exactly the rows that must come back under the semantics MOTIR-3294 states: "empty on an axis means unconstrained on it, and the three are independent", restated by MOTIR-3300 as "a query naming onlytypesdoes not constrainkindsorphases". Assertions 1 and 4 read identically under both readings, which is why the table looked coherent.The reading is not a preference. Under the original table a lesson tagged on ONE axis vanishes from every query naming ANOTHER — so
notes.html #37, taggedtypes: [test]by MOTIR-3299's backfill, would be invisible to everykindsorphasesquery, and the more precisely the corpus is tagged the less of it a planner can reach. That inverts the epic. The corrected sets are below and the same reasoning is carried at the head of the shipped suite.
Seed five rows, each present for one reason:
| row | types | kinds | phases | why |
|---|---|---|---|---|
| A | design | — | deepen | single value on two axes |
| B | code | — | deepen | must NOT return for a design query |
| C | code, chore, deploy | — | — | the set case — overlap, not equality |
| D | — | — | — | unconstrained — must reach every query |
| E | — | bug | skeleton | the widening case, and kind-only |
Assert:
types: [design], phases: [deepen]returns A and D; not B, C or E. (Unchanged — A matches on both axes, D is unconstrained on both, and each of B/C/E is genuinely excluded by one of the two clauses.)types: [chore]returns C, D and E — C matches only by overlap; D and E carry notypesat all and are therefore unconstrained on that axis. A (design) and B (code) are excluded.phases: [skeleton]returns C, D and E, and not A or B — widening and deepening lessons are genuinely separated, which is the axis with no predecessor. C and D carry nophasesand stay eligible; A and B aredeepenand must not reach a widening pass.- No axes supplied returns all five — today's behaviour, unchanged.
kinds: [bug]does not constraintypesorphases: E matches by overlap and A, B, C and D all carry nokinds, so all five return. The clause is not vacuous —kinds: [epic], which no row claims, still excludes E while leaving the four unconstrained rows.
And, because D makes an empty selection unreachable while it is in the store, the empty-selection no-op is proved on a store seeded with A and B ALONE.
Assert on returned lesson ids, not the rendered promptSection — later stories edit that template.
Acceptance criteria
- All assertions pass against the real test database, rows seeded through the service rather than by raw insert.
- The suite goes red when any one axis applies an empty array instead of omitting its clause — demonstrated once per axis, with the failures in the PR body. That is the mutation that makes every unconstrained lesson unreachable, and there are now three ways to introduce it.
- The suite ALSO goes red when any one axis drops the lesson-side
cardinality(col) = 0arm — the second, independent way to make an unconstrained lesson unreachable, and the one this run actually hit. Demonstrated once per axis. mistakeTypesfiltering is asserted unchanged alongside the new axes, and asserted to COMPOSE with an axis clause rather than be replaced by one.- The empty-selection no-op holds: axes matching no row yield an empty
promptSectionand no injection. - The axes survive the read as real arrays. A
$queryRawover a user-defined ENUM ARRAY returns the raw Postgres literal string unless it is cast, and every filter still passes in that state, so this is asserted on the read surface. - Only the changed files are run locally; CI runs the suite.
Context refs
motir-aitests/lessonInjection.test.ts— the sibling suite whose seeding helpers andembed()mock this mirrors.motir-aisrc/repositories/lessonRepository.ts— the three overlap clauses under test.- MOTIR-3300 — the filters and the seam this exercises.
- MOTIR-3355 — the planning bug for the amended fixture.