@learnquest/learner-smoke
Learner-view smoke test: plays every live (app, language, learner locale) tree the way a learner does and asserts on what is rendered
Package Info
Versions
| Version | Published |
|---|---|
| 0.7.3 | 2026-10-25 |
| 0.7.2 | 2026-10-25 |
| 0.7.1 | 2026-10-24 |
| 0.7.0 | 2026-10-24 |
| 0.6.2 | 2026-10-24 |
| 0.6.1 | 2026-10-24 |
| 0.6.0 | 2026-10-24 |
| 0.5.0 | 2026-10-23 |
| 0.4.0 | 2026-10-20 |
| 0.3.0 | 2026-10-19 |
| 0.2.0 | 2026-10-19 |
| 0.1.0 | 2026-10-19 |
Per-version supply chain
| Version | Sig | SBOM | Att |
|---|---|---|---|
| 0.7.3 | |||
| 0.7.2 | |||
| 0.7.1 | |||
| 0.7.0 | |||
| 0.6.2 | |||
| 0.6.1 | |||
| 0.6.0 | |||
| 0.5.0 | |||
| 0.4.0 | |||
| 0.3.0 | |||
| 0.2.0 | |||
| 0.1.0 |
Click a version to view its details.
Supply chain
Provenance
- Published by
- web:14
- When
- 2026-10-25T08:25:06Z
- sha256 (registry-computed)
- ec6b257d83c61aa565f9346a9e4691bbe03242079b9c8d6e4d366da12bb578fe
Observed by the registry at publish.
Claimed by the build
The publisher claimed nothing about its build.
Attachments
- Signature
- none
- SBOM
- none
Dependencies
README
@learnquest/learner-smoke
Learner-view smoke test (post-beta roadmap P1, lq-core docs/design/2026-10-19-post-beta-roadmap-specs.md). It plays
every live (app, language, learner locale) tree the way a learner does and asserts on what is rendered, not on
files. File-level gates were green while learners saw hidden lessons, English answer options in th/zh trees,
broken card art and silent audio; this is the check that looks at the screen.
Usage
# every live tree of every lq-* checkout under --root (default: $LQ_ROOT, or the parent of an lq-* cwd)
learner-smoke --app all --root ~/Repos-internal/learn-quest --workers 4 --json smoke.json --md smoke.md
# one app, some trees
learner-smoke --app lq-germanic --lang german --locale en,th
# a local build (manifests from the working tree, app at a local URL)
learner-smoke --app lq-germanic --worktree --root <dir> --base-url http://localhost:4173 --lang german --locale en
# print the matrix only
learner-smoke --app all --list
Boot check for dependency-bump PRs
vite build and unit tests do not prove that the app starts. lq-germanic #96 (a drill-engine override) passed both,
and production was blank at boot: "Cannot set properties of undefined (setting 'pluralize')". Attach this to every
dependency-bump PR as evidence. It takes a few seconds:
# in the app worktree with the bump
pnpm build
npx vite preview --port 4181 --strictPort & # any free port
learner-smoke --app lq-germanic --worktree --root "$PWD" --base-url http://localhost:4181 --boot-only
# boot pass lq-germanic http://localhost:4181/ (1.1 s): booted in 1.0 s (378 chars rendered)
# boot FAIL lq-germanic http://localhost:4181/ (2.0 s): page error at boot: Cannot set properties of undefined (setting 'pluralize') (2.0 s)
--boot-only opens one fresh headless context per app origin and waits up to ~15 s for the consent gate or another
app marker. It fails on an uncaught page error, an unhandled promise rejection, an HTTP error for the document, or an
empty #root, and it reports the first page error verbatim. With --lang/--locale it boots /<locale>/ once per
(app, lang, locale) instead. The output is one line per target and then the JSON (or the JSON goes to --json <file>).
Exit 0 means everything booted; exit 1 means a boot failed. --app all --boot-only checks every deployed app.
--root takes a directory of lq-* checkouts, or a single checkout or worktree (any folder name, e.g.
lq-germanic.worktrees/my-bump). In the second case --app names it.
| Option | Default | Meaning |
|---|---|---|
--app <repo|all> | required | lq-germanic or germanic; comma list; all = every lq-* checkout with blueforge.config.json and languages/ |
--lang, --locale | all | narrow the matrix (comma lists) |
--base-url | https://<apps.web.domains[0]> | the app origin (one --app only) |
--workers | 4 | parallel trees (one Chromium, one fresh context per tree) |
--json, --md | none | full report (every step's detail and evidence) / markdown table |
--out | ./learner-smoke-results | screenshots of failing steps (screens/<app>-<lang>-<locale>-<step>.png) |
--ref / --worktree / --fetch | origin/main | where manifests are read: a git ref via git show (local main checkouts are often stale), or the working tree; --fetch runs git fetch origin first |
--root | $LQ_ROOT, or the parent of an lq-* cwd | a directory of lq-* checkouts, or one checkout/worktree itself |
--until boot|picker | off | stop each tree after that step (e.g. a quick card-art sweep: --app all --until picker) |
--boot-only | off | only the boot check (see above); seconds, exit 0/1 |
--max-chapters N | all shown | open at most N chapter maps per tree |
--no-retry, --no-screenshots, --headed |
Exit 0 when no step failed, 1 when one did, 2 on a usage or setup error. Needs Chromium for the resolved
playwright (npx playwright install chromium once on a fresh machine).
The matrix
From the repos, never a list: per app blueforge.config.json (apps.web.domains[0]) and per language
languages/<lang>/data/locale-manifest.json. A course is live when the manifest does not set enabled: false, en
is enabled and en's maturity is not retired; its trees are the enabled, non-retired locales. shipChapters comes
from the top level, else from locales.en; none means "every published chapter".
What one tree does
A fresh browser context (browser locale = the learner locale) and no account: nothing is posted; progress lives in the context's localStorage and dies with it.
| Step | Pass when |
|---|---|
boot | runs first: the origin loads headless, the consent gate or app root renders within ~15 s, with no uncaught page error or unhandled rejection and a non-empty #root. If it fails, every other step is skip ("skipped: boot failed") with the first page error in the detail |
locale | the app opens at /<locale>/ for the browser locale, or the top-bar selector switches the URL there (en: skip). On failure the run continues at /<locale>/ directly |
picker | consent gate ("Before you start": 18+ checkbox, Start learning), then the "What do you want to learn?" card language-option-<lang>. Its art (sections[].image in /data/lessons/<locale>/index.json) is the card <img> whose src ends with that path (else the largest img); it passes when it decodes or when its URL answers 2xx image/*, and fails only on a non-image answer (SPA html fallback, 404) or a decode error, naming currentSrc, status and content-type. No art declared = skip (P13). With one language for the locale the app skips the picker: pass |
onboarding | welcome slides and the character step end on /<locale>/<lang>/chapters/.... When the pick is lost and a second picker follows, the run picks again and continues, and the step fails |
avatars | character creation offers at least 5 portraits per gender (counted from the served /data/avatars/player/<gender>/<style>/index.json), the portrait renders, and it was fetched from the blob host rather than the app origin. A gender with three portraits is not a choice |
chapters | the chapter list shows min(shipChapters, published) cards (published = served chapters.json), and no chapter map is empty. Chapter 1 is opened from the list, later ones by route change |
visible | every enabled lesson of those chapters is on its map and released or beta. An unreviewed lesson (aiDraft without reviewedAt, no releaseStage: "beta", via @learnquest/content-audit/release) fails either way: hidden from the map (newer runtimes), or still on the map without a Beta badge (web-app 0.5.x maps do not apply the gate); status: "draft" is reported as intent |
lessons | ch1 lesson 1 opens from the map (node, then Start), and the first concept, conversation (or dialogue) and quiz of ch1 render (deep links: they are locked on a fresh profile). Screens are stepped through, audio buttons pressed |
drill-xp | the first ch1 drill is completed with the correct answers from the served JSON, through the stage screens to the reward screen, and the map's XP readout goes up |
audio | every audio URL requested meanwhile answers 200/206 with audio/* (request interception, no playback; an SPA html fallback fails). Buttons pressed but nothing requested = warn |
console | no console error and no uncaught page error |
placeholders | no [Name], {name}, {{name}} in visible text |
en-options | th/zh/de: no MC question on screen whose options are the untranslated English ones (@learnquest/content-audit/english-options, classified on the en prompt like optcount.py) |
beta-badge | lessons stamped releaseStage: "beta" show level-node-<id>-beta, others do not |
da-frames | non-Danish course: the usage drills (drill-card-<templateId>) of the first ch1 lesson that publishes morphology show no Danish frame ("Jeg har", "Jeg ser", "Hvor er", "Det er"). lq-core's drill engine fell back to Danish templates for morphology without a course language (2026-10-19). Norwegian shares the wording, so a da.* template there is a warning |
| blob-origin | every media request the run made (image, audio, font) came from the blob host (VITE_ASSETS_BASE_URL, minio-backed), not from the app origin. An app serving its own media is serving a stale copy or bypassing the CDN entirely |
A tree whose boot, picker, onboarding, chapters, visible, lessons, drill-xp or audio failed is played once more
from scratch (deploy lag); the second attempt is the result and the report says (retried). The other steps are
verdicts on the same content and are not replayed.
Not automated
- Lessons after lesson 1 are opened by deep link, not by playing every earlier lesson to unlock them.
- Only multiple-choice drills are completed; a drill with typed or ordering questions is
skip. - The en-options check sees only questions on the screens visited (ch1 lesson 1, concept, conversation, quiz, drill).