Skip to content

Migrated 80 ghost/core unit tests to vitest (bin/shared/adapters/data/lib/web)#27900

Merged
9larsons merged 5 commits into
mainfrom
vitest-ghost-core-unit-safe-buckets
May 19, 2026
Merged

Migrated 80 ghost/core unit tests to vitest (bin/shared/adapters/data/lib/web)#27900
9larsons merged 5 commits into
mainfrom
vitest-ghost-core-unit-safe-buckets

Conversation

@9larsons
Copy link
Copy Markdown
Contributor

@9larsons 9larsons commented May 14, 2026

Summary

Second in the planned sequence migrating ghost/core's tests from mocha to vitest (after #27898). Expands vitest to cover test/unit/{bin,shared,server/{adapters,api,data,lib,web}} — 81 files / 824 tests.

Update (post-review): the original approach ran each migrated file under both runners as a safety net. That broke down on lexical.test.js — see findings below. Switched to vitest-only for migrated buckets: each bucket migration removes paths from mocha's run list and adds them to vitest's include glob. No more dual-compat constraint on individual files; future migrations are mechanical.

What's changed

  • vitest.config.tsinclude expanded to the new buckets; six done()-callback files stay excluded (deferred to a focused follow-up; ~50 callbacks to convert to promises)
  • package.jsontest:unit:base now lists the remaining mocha paths explicitly (unmigrated dirs + the six deferred done() files), instead of scanning all of ./test/unit
  • vitest-setup.ts — kept the snapshot/port normalization and the bridged mochaHooks; no before/after shim
  • test/.eslintrc.js — added beforeAll/afterAll to globals so the vitest-only files lint cleanly
  • .c8rc.json — disabled the coverage gate (reporters still run). See "Coverage gate" below.

Findings from this bucket

  1. describe(name, {options}, fn) is not valid Mocha syntax. Vitest accepts it, but Mocha treats the options object as the fn arg — the describe registers 0 tests and leaves the suite stack open, so subsequent files (e.g. mrr.test.js) end up nested under it. That's the source of the 14 failures on the previous run. Reverting lexical.test.js to its original this.timeout(5000) would have worked in mocha but failed in vitest (this is undefined), confirming there was no clean dual-compat option. Fix: lexical keeps the vitest-native {timeout: 5000} syntax and runs under vitest only.
  2. before / after aren't vitest globals. Original PR aliased them in vitest-setup.ts. Removed the shim and converted the 5 affected files in migrated buckets to beforeAll/afterAll (storage/index, scheduling/utils, web/shared/middleware/brute, shared/config/loader, shared/config/adapter-config).
  3. Three files use mocha-this patterns inside describeslexical.test.js (this.timeout(5000)), sentry.test.js and importer/index.test.js (this.beforeEach(fn)). The this.beforeEach calls were already mocha-API misuses (they happen to work because mocha binds this to the Suite object); replaced with top-level beforeEach(fn).
  4. 6 files use the mocha done() callback — vitest 4.x removed support. Listed under exclude in vitest.config.ts and explicitly included in test:unit:base so they continue to run under mocha. Deferred to a focused follow-up PR:
    • test/unit/server/adapters/scheduling/scheduling-default.test.js
    • test/unit/server/adapters/storage/local-images-storage.test.js
    • test/unit/server/lib/image/blog-icon.test.js
    • test/unit/server/lib/image/gravatar.test.js
    • test/unit/server/lib/package-json/parse.test.js
    • test/unit/server/web/api/middleware/cors.test.js

Coverage gate

c8 only sees mocha's run. Moving tests to vitest shrinks c8's covered-files count while the denominator (all source files in core/) stays constant, so coverage % drops on every migration PR even though true coverage is unchanged. If the gate stays on, each PR has to lower the threshold — pure noise.

Proper fix is to merge vitest's coverage into c8's report, but two blockers:

  • @vitest/coverage-v8 instrumentation conflicts with the tsx loader: require('./bulk-filters') fails to resolve .ts files when coverage is enabled
  • Vitest's forks pool workers don't flush native V8 coverage on exit (NODE_V8_COVERAGE propagates, but the worker doesn't write its coverage-<pid>.json before being torn down)

For now: check-coverage: false, reporters left on so numbers stay visible (~63% statements, ~62% functions, ~85% branches at this point). Re-enable a merged gate once the migration completes or a vitest+tsx coverage combo becomes viable.

Test plan

  • pnpm test:vitest (from ghost/core): 81 files / 824 tests pass (+ 3 skipped)
  • pnpm test:unit:base (mocha): 5689 tests pass — no more cross-file pollution from the broken describe options syntax
  • pnpm exec eslint clean for changed files
  • CI green

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 14, 2026

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 413eb04f-e125-41b2-bdcd-57fcdcce1240

📥 Commits

Reviewing files that changed from the base of the PR and between acaf5f9 and 141fe7d.

📒 Files selected for processing (1)
  • ghost/core/.c8rc.json
✅ Files skipped from review due to trivial changes (1)
  • ghost/core/.c8rc.json

Walkthrough

Updates Vitest configuration and test infra, disables strict c8 coverage thresholds, and adjusts the unit test script. Multiple unit test files are converted to Vitest-compatible patterns: suite timeouts moved to describe options, Mocha hook registrations standardized, before/after hooks replaced with beforeAll/afterAll where appropriate, and arrow callbacks changed to function() when using Mocha-style this bindings.

Possibly related PRs

  • TryGhost/Ghost#27898: Introduced initial Vitest config and setup that this change extends.
  • TryGhost/Ghost#27901: CI/automation changes that run Vitest and align with the updated vitest.config.ts and test script selection.

Suggested reviewers

  • EvanHahn
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: migrating 80 ghost/core unit tests to vitest across the specified directories (bin, shared, adapters, data, lib, web).
Description check ✅ Passed The description is comprehensive and directly related to the changeset, explaining the migration strategy, findings, configuration changes, and test results.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch vitest-ghost-core-unit-safe-buckets

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

…/lib/web)

- expanded vitest's include glob to cover test/unit/{bin,shared,server/{adapters,data,lib,web}}
- 80 files / 821 tests pass under vitest; PR 1's 4 files still included
- mocha continues to run the same files in parallel (no mocha-side changes) — the 11s vitest cost on top of the existing mocha unit run is negligible, and dual-running gives us a safety net through PR 14 when mocha is removed entirely
- added before/after shim to vitest-setup.ts so mocha's before/after aliases work as beforeAll/afterAll without rewriting every test file
- per-file mocha-this fixes for the 3 files that needed them: lexical.test.js (this.timeout → describe-level options), sentry.test.js + importer/index.test.js (this.beforeEach → top-level beforeEach — these were technically already misuses in mocha that happened to work)
- 6 files using the mocha done() callback are excluded from vitest for now; vitest 4.x removed done() support, and converting ~50 callbacks to promises is its own focused follow-up PR. Listed explicitly in vitest.config.ts under exclude.
@9larsons 9larsons force-pushed the vitest-ghost-core-unit-safe-buckets branch from 6d2c34d to 8e9b3a0 Compare May 14, 2026 17:14
9larsons added 3 commits May 18, 2026 19:43
- the previous dual-runner approach in #27898/#27900 required every migrated
  file to be cross-compatible with both mocha and vitest, which broke down
  on lexical.test.js: `describe(name, {timeout}, fn)` is valid vitest syntax
  but mocha treats the options object as the `fn` arg, registers 0 tests,
  and leaves the suite stack open so subsequent files (mrr.test.js) get
  nested under it — that's the source of the 14 CI failures
- shimming `before`/`after` onto vitest globals papered over one symptom but
  encouraged more dual-compat workarounds across the migration; dropping the
  shim and converting the 5 affected files to `beforeAll`/`afterAll` is the
  cleaner path
- mocha now skips migrated bucket dirs (bin, shared, server/{adapters,api,
  data,lib,web}) while still running the 6 deferred `done()`-callback files
  explicitly; vitest is the sole runner for the migrated buckets
- future migration PRs just shrink the mocha path list and grow the vitest
  include list, with no dual-compat constraint on individual test files
- moving 800+ tests from mocha to vitest in the prior commit dropped c8's
  numerator (it only instruments the mocha run) while leaving the
  denominator unchanged because c8's "all": true counts every source file
  in core/ regardless of whether mocha touches it
- result: c8 reports 62.84% lines / 61.83% functions, below the old 65%
  gate even though true coverage (mocha + vitest combined) is unchanged
- proper merge of vitest's v8 coverage into c8 isn't viable in this PR:
  vitest's `forks` pool workers don't flush V8 coverage on exit, and
  enabling @vitest/coverage-v8 breaks `require()` of `.ts` files (tsx
  loader hook conflict)
- pragmatic fix: lower statements/functions/lines from 65 to 60 to match
  the mocha-only baseline. Branches (85) is unchanged — still passes at
  85.47%. Worth a follow-up to merge coverage properly once a vitest/tsx
  combination supports it
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
ghost/core/.c8rc.json (1)

9-12: ⚡ Quick win

Add an explicit expiration/follow-up marker for lowered thresholds.

Lowering gates is understandable for this migration, but this can easily become permanent drift. Please add a nearby comment (or linked issue ID in commit/PR metadata) with a clear restore target so the 60% thresholds are temporary by contract, not convention.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ghost/core/.c8rc.json` around lines 9 - 12, The coverage thresholds for
"statements", "branches", "functions", and "lines" are temporarily lowered to
60%; add an explicit expiration/follow-up marker so this change isn’t permanent
drift by adding a clear adjacent comment or a linked issue/PR ID noting the
restore target and date (e.g., restore thresholds to previous values or X% by
YYYY-MM-DD) and reference the exact keys ("statements", "branches", "functions",
"lines") in that comment so reviewers and automation can find and enforce the
rollback.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@ghost/core/.c8rc.json`:
- Around line 9-12: The coverage thresholds for "statements", "branches",
"functions", and "lines" are temporarily lowered to 60%; add an explicit
expiration/follow-up marker so this change isn’t permanent drift by adding a
clear adjacent comment or a linked issue/PR ID noting the restore target and
date (e.g., restore thresholds to previous values or X% by YYYY-MM-DD) and
reference the exact keys ("statements", "branches", "functions", "lines") in
that comment so reviewers and automation can find and enforce the rollback.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ff81a8f1-325f-4f39-90ea-61860b6ff9d0

📥 Commits

Reviewing files that changed from the base of the PR and between 87a1256 and acaf5f9.

📒 Files selected for processing (1)
  • ghost/core/.c8rc.json

- c8 only sees mocha's run; every PR that moves a bucket from mocha to
  vitest shrinks c8's covered-files count while the denominator (all
  source files in core/) stays constant
- if the gate stays on, each migration PR has to lower the threshold to
  match — that's noise that masks real regressions
- proper fix is to merge vitest and mocha coverage, but @vitest/coverage-v8
  conflicts with the tsx loader (require'd .ts files fail to resolve) and
  vitest's forks pool workers don't flush V8 coverage on exit, so the
  merge isn't viable yet
- removed thresholds and set check-coverage: false; reporters stay so the
  numbers are still visible. Re-enable a merged gate once the migration
  completes
@9larsons 9larsons merged commit 9ddaff0 into main May 19, 2026
43 checks passed
@9larsons 9larsons deleted the vitest-ghost-core-unit-safe-buckets branch May 19, 2026 18:21
9larsons added a commit that referenced this pull request May 20, 2026
…27974)

no ref

Continues the vitest migration started in #27898 / #27900. Converts 5 of
the 6 test files that were deferred because they used mocha's `done()`
callback (vitest 4.x dropped support), and moves them from mocha to
vitest.
9larsons added a commit that referenced this pull request May 20, 2026
no ref

Continues the vitest migration (after #27898 / #27900 / #27974) by
moving the `test/unit/api` bucket — 33 files, 280 tests — from mocha to
vitest.
9larsons added a commit that referenced this pull request May 20, 2026
no ref

Continues the vitest migration (after #27898 / #27900 / #27974 / #27991)
by moving the `test/unit/frontend` bucket — 135 files, 1804 tests — from
mocha to vitest.
9larsons added a commit that referenced this pull request May 20, 2026
no ref

Continues the vitest migration (after #27898 / #27900 / #27974 / #27991
/ #27992) by moving the `test/unit/server/models` bucket — 34 files, 269
tests — from mocha to vitest.
9larsons added a commit that referenced this pull request May 21, 2026
no ref

Final big bucket of the vitest migration (after #27898 / #27900 / #27974
/ #27991 / #27992 / #27996) — moves `test/unit/server/services` (256
files, ~3290 tests) from mocha to vitest.

It also fixes a **vitest worker-teardown bug** that this DB-heavy bucket
exposed (the previous buckets happened not to trigger it).
9larsons added a commit that referenced this pull request May 21, 2026
no ref

Finishes the `ghost/core/test/unit` migration from mocha to vitest
(following #27898 / #27900 / #27974 / #27991 / #27992 / #27996 /
#28009). The last 3 mocha holdouts now run on vitest, so **the mocha
unit-test path is retired** — all of `test/unit` (550 files / 6573
tests) runs on vitest.
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