Two breakages that the typecheck failure was hiding, since every step after it
in the "Test development" job was skipped and the CKEditor job never got as far
as uploading anything.
The root config globs both `packages/*/vitest.config.ts` and
`packages/*/vite.config.ts`. ckeditor5 is the only package with both, and its
vite.config.ts merely builds the library — it declares no tests at all. Vitest 4
registered it anyway as a second project sharing the package's name; vitest 5
rejects the duplicate outright and `pnpm scripts:test` cannot start:
Error: Project name "@triliumnext/ckeditor5" from
"packages/ckeditor5/vitest.config.ts" is not unique.
Blob reports also moved from `.vitest-reports/` to `.vitest/blob/`, alongside
the attachments that moved to `.vitest/`. The shard jobs were writing them where
nothing looked, so the upload failed with "No files were found" — after the
tests themselves had passed. The merge job reads the same path and would have
failed next.
Verified by running what CI runs: both shards, then `--mergeReports --coverage`,
which replays 127 files / 1626 tests and clears the gate at
99.91/99.54/100/99.98.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Vitest 5 handed the webdriverio provider to community maintenance, and no
stable 5.x of `@vitest/browser-webdriverio` was ever published — npm `latest`
is still 4.1.11, with only a 5.0.0-rc.1 from before vitest 5.0.0 shipped.
Pairing that v4 provider with `@vitest/browser` 5 kills the suite at startup:
TypeError: Cannot read properties of undefined (reading 'project')
at createBrowserServer (@vitest/browser/dist/index.js:7900:26)
`@vitest/browser-playwright` 5.0.0 is stable and first-party, and Playwright
1.62.1 is already here for the e2e suites, so the browser toolchain costs
nothing new. Only two of the 129 specs touch provider-visible API at all, both
through the provider-agnostic `userEvent`.
Playwright drives the browser over CDP with no separate driver, so the
chromedriver half of the NixOS workaround goes away: the dev shell keeps
`pkgs.chromium` and `CHROME_BIN`, which now reaches the provider as
`launchOptions.executablePath`. CI installs the browser in a step of its own
rather than inside the test step, whose 15-minute cap exists to catch a browser
session that never starts and should not also have to cover a 190 MB download.
Two specs depended on webdriverio behaviour and are fixed rather than skipped:
- The token-cost assertion read `1.234` where it wanted `1,234`.
`toLocaleString()` takes the browser's locale, and Playwright inherits the
host's where the old Chrome defaulted to en-US. Pinning `contextOptions.locale`
keeps the suite from depending on the developer's machine.
- The format painter's drag-selection selected nothing. Playwright's
`dragAndDrop` turns on drag interception, so the press reaches the page as an
HTML5 drag intent instead of selecting text. Driving the press, move and
release over CDP restores what the test is actually for: proving a *native*
pointer interaction has updated the model selection by the time the `mouseup`
listener runs.
Vitest 5 also moved failure screenshots from `.vitest-attachments` to
`.vitest`, which needs ignoring.
Verified: 127 files / 1626 tests green, coverage 99.91/99.54/100/99.98 against
the 99.5 gate. The Nix path is unexercised — `executablePath` is ordinary
Playwright, but it was not run from a dev shell.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The migration from codecov/test-results-action to codecov-action was meant
to be an action swap with report_type added, but it also dropped
`if: ${{ !cancelled() && steps.test-<pkg>.outcome != 'skipped' }}` from all
nine upload steps.
Without the guard each upload runs even when its test step never did.
test_server and test_standalone gate their test steps on the paths-filter
`AFFECTED == 'true'`, so a PR touching neither area now reaches the upload
with no junit.xml produced in this run.
Restored verbatim from main at all nine sites: client, desktop, commons,
codemirror, highlightjs, pdfjs-viewer, server, standalone, ckeditor5.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
actions/checkout writes the workflow token into .git/config by
default. The update workflows push through the create-pull-request
action's own token input and the releases publish with dedicated
credentials, nothing pushes with the persisted token, so every
checkout now sets persist-credentials false.
Every third party action now points to a full commit SHA inside the
major already in use, tag kept in a comment. A tag is a movable
pointer, whoever controls the action repository can repoint it and
the next run executes their code with this repo's secrets, the
release, signing and registry credentials included, the March 2025
tj-actions/changed-files incident (CVE-2025-30066) worked exactly
that way. The archived codecov/test-results-action is replaced by
codecov-action with report_type test_results, the migration codecov
documents themselves, no archived code left in the pipeline.
Renovate keeps SHA pins updated through the same manual-merge PRs it
opens for tags today.
build_docker waited on all four test jobs, which put its ~4m chain
(build_docker -> test_docker) in series behind the slowest suite. It
validates that the client and server build and that the image assembles —
none of which depends on a test outcome — so the wait bought no signal.
Measured against the last green run, this takes the workflow from 11m00s
to the length of its longest job, test_dev at 7m02s. The Docker chain at
3m44s now finishes comfortably inside that.
test_docker keeps its dependency on build_docker: build_docker populates
the type=gha buildx cache it reads, the pair already fits inside the
slowest suite, and breaking the edge would leave both jobs racing to write
the same cache.
The trade is that a push whose tests fail still spends runner time
building images. That is the accepted cost of taking Docker off the
critical path; the workflow's concurrency group still cancels superseded
runs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HT4fNrMrMxu64SDsHUckF5
Both shards ran and wrote their blobs correctly —
blob report written to .../packages/ckeditor5/.vitest-reports/blob-2-2.json
— but the upload matched nothing, because .vitest-reports is dot-prefixed
and upload-artifact skips hidden paths unless include-hidden-files is set.
if-no-files-found: error surfaced it as a failure rather than letting the
merge job fail later on an empty blob directory.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HT4fNrMrMxu64SDsHUckF5
The suite was already split into two shards for renderer memory, but ran
them back-to-back in a single test_dev step. Move it into its own jobs so
the shards run concurrently, and gate it on whether the change can affect
it — at 75% of recent commits skipped, it is the narrowest filter in the
workflow, depending only on commons.
Isolating it also means a webdriverio session failure — the suite drives a
real headless Chrome and is the most infrastructure-sensitive of the
suites — no longer blocks the client and package suites, and can be
re-run on its own.
A merge job replays the shards' blob reports to produce junit/lcov and to
evaluate the package's real coverage thresholds over the whole suite, so
the gate still spans every spec rather than each shard. Vitest names blobs
.vitest-reports/blob-<index>-<count>.json, so the shards' artifacts cannot
clobber each other when merge-multiple flattens them.
The filters are restructured so the workspace-wide roots are their own
anchor: ckeditor5 picks those up without inheriting the much wider
trilium-core closure that server and standalone need. Note this filter
does not guard the t()/translation-key contract — the spec enforcing it
lives in apps/client and runs in the ungated test_dev job.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HT4fNrMrMxu64SDsHUckF5
v3 runs on the node20 action runtime, which the runner now warns is
deprecated. v4.0.0 is a runtime-only bump to node24 — the `filters`
input and the outputs are unchanged, so the filter definition carries
over as-is.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HT4fNrMrMxu64SDsHUckF5
The server (~4m10s) and standalone (~3m30s) suites were the two slowest
steps in test_dev and ran serially behind the client and package suites.
Split them into their own jobs so all three run concurrently, and gate
each on whether the change can actually affect it.
Both suites include ../../packages/trilium-core/src/**/*.spec.ts — the
standalone suite exists to re-run that same set under happy-dom + sql.js
— so the shared filter covers trilium-core and everything it pulls in
(commons, turndown-plugin-gfm, backup-container, highlightjs) plus the
workspace roots and the pnpm patches. Replayed over the last 224 commits
the filters skip ~58% of runs with no false negatives.
Each job always starts and gates its steps rather than carrying a
job-level `if:`. A job skipped at the job level reports no result to a
required status check and propagates the skip through `needs:` to
build_docker; gating the steps keeps the job green and the dependency
graph intact. build_docker still waits on all three suites, which costs
nothing since test_server is the longest.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HT4fNrMrMxu64SDsHUckF5
nixpkgs lags behind the Electron releases we track, so flake.nix builds the
exact pinned version from Electron's official binary release. Renovate bumps
the devDependency but knows nothing about that pin, so every Electron update
left the flake refusing to evaluate until someone refreshed six hashes by
hand.
scripts/update-flake-electron.ts does it instead: the per-platform checksums
come from the release's own SHASUMS256.txt and the headers hash from
nix-prefetch-url --unpack. It is a no-op when the pin already matches.
The workflow runs it on pushes to main that touch apps/desktop/package.json
(and on demand), verifies the written hashes by actually building .#electron
and .#electron.headers, and only then opens a PR.
Two supporting flake changes:
- packages.electron is exposed, so the pinned binary can be built — and its
hashes verified — without a full desktop build.
- electron_<major> is looked up with `or null`. A major bump lands in
package.json long before nixpkgs has the attribute, and without the
fallback the flake died with an attribute error rather than reaching the
throw that explains the mismatch.
scripts/ is not a workspace package, so its tests get their own vitest
project, a `pnpm scripts:test` entry point, and a step in dev.yml.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nothing imports it any more: its only two consumers were the open-partial
routes, which now run the transport-neutral range handler in core so that
standalone gets them too.
Drops the package along with its workspace dependency, three tsconfig
project references, its CI test and Codecov steps, the coverage flag, the
Nix components entry and the two monorepo structure listings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two runs of this branch died at the same point: 100 of 121 spec files
completed, then the browser session either dropped mid-run or stopped
responding until the step timed out. Chromedriver and Chrome are
correctly paired (150.0.7871.124 / .128), and the DOM is clean at the
end of every file — no editors, math fields, balloons or previews are
left behind, so the tests themselves are not leaking.
What accumulates is v8 coverage data. Browser mode keeps one page for
the whole run, and coverage collects for every script loaded into it.
Measured locally over the same file order:
file without --coverage with --coverage
20 218 MB 235 MB
60 291 MB 498 MB
100 236 MB 847 MB
106 - 873 MB (peak)
Without coverage the heap sawtooths and GC keeps up. With it, the heap
climbs to ~3.6x by the 100th file — exactly where CI gives out. Locally
there is enough headroom to survive it; a runner has less.
Run the suite as two shards of ~60 files, which peaks around 500MB, and
merge the blob reports afterwards. Thresholds are zeroed for the shard
runs (each only exercises half the tree, so they would fail on files the
other shard covers) and enforced on the merged report, which reproduces
the single-run numbers exactly and still writes the JUnit file Codecov
uploads.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The aggregate suite drives a real headless Chrome through webdriverio.
Two runs of identical code failed in different ways — one dropped the
session mid-run (19 of 121 files never reported), one produced nothing
for 15 minutes before being cancelled — which is what a chromedriver
that does not match the installed Chrome looks like: the session
handshake fails, and vitest waits on the browser rather than erroring.
Print both versions before the run so the pairing is visible in the log,
and cap the step at 15 minutes (it takes ~90s on main) so a stuck
session fails fast instead of holding the runner until the job's default
timeout.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The last of the ckeditor5-* packages. `packages/ckeditor5-math` had no
consumers outside `@triliumnext/ckeditor5`, was never published, and
carried a ckeditor5-package-generator scaffold that nothing invoked —
including a tsconfig.test.json referencing an uninstalled @types/mocha,
so its tests were never typechecked at all.
Move it to src/plugins/math/, flattening src/ui/ and renaming to the
repo's snake_case convention, with the tests co-located as *.spec.ts.
The `mathlive` and `@ckeditor/ckeditor5-icons` dependencies move onto
this package, which is now the only thing that needs them.
Three things the move surfaced:
- math_input_view.ts re-declared `window.mathVirtualKeyboard`, which
MathLive declares itself. Harmless in a separate package, a hard
TS2717/TS2687 here — and it was cascading into a dozen unrelated
typecheck errors across apps/client and apps/build-docs.
- MathUI binds `mathInputView#value` to the command, but MathInputView
assigns its own `value` on every MathLive/textarea change, which
severs the binding. A legacy test asserted the live binding and had
been passing only by accident of timing. It now asserts the contract
that holds, and _addFormView()'s seemingly redundant manual push is
documented as load-bearing.
- The preview element is never actually hidden or removed by hand:
both call sites look it up after the view containing it has already
gone. Marked as dead rather than left looking functional.
Coverage: lines and functions 100%, statements 99.91%, branches 98.91%.
The residue is branch permutations in the MathLive sync logic
(main_form_view, math_input_view, math_ui) and still needs tests before
the aggregate's 100% gate passes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`packages/ckeditor5-mermaid` had no consumers outside
`@triliumnext/ckeditor5`, declared no dependencies of its own — the
mermaid library is supplied by the host through `config.mermaid.lazyLoad`
— was never published, and carried a ckeditor5-package-generator scaffold
(sample pages, manual test fixtures, its own ESLint / Stylelint / vitest
configs, typings) that no root script or CI job ever invoked.
Move it to src/plugins/mermaid/, renamed to the repo's snake_case
convention with the commands/ subdirectory flattened, matching how the
admonition, collapsible and footnotes plugins are laid out. The icons
gain a `mermaid-` prefix because `info.svg` and `insert.svg` are too
generic for the shared src/icons/ folder.
`augmentation.ts` folds into mermaid.ts alongside the glue plugin — the
PluginsMap entries, the `EditorConfig.mermaid` shape and the global
MermaidInstance/MermaidConfig types — and the barrel is dropped, so
consumers import INSERT_MERMAID_COMMAND and MermaidSample directly. The
`icons` export the barrel provided had no consumers and goes with it,
along with the DLL test that asserted it.
src/index.ts keeps a side-effect import of the plugin: it is what forces
the module augmentation into the aggregate's emitted declarations, and
without it the client fails to typecheck `config.mermaid`.
The package arrived at 100% coverage (4a32394001) and holds it here under
the aggregate's gate: 107 tests, no uncovered lines in the moved code.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`packages/ckeditor5-utils` held a single module, ContentHintManager,
consumed only by two plugins inside `@triliumnext/ckeditor5`. It was
never published, and the package.json / two tsconfigs / vitest config
around those 476 lines cost more than they bought — the same
scaffolding-per-module problem as the plugin packages folded in before
it.
Move content_hint_manager.ts and its spec to the aggregate's src/, next
to utils.ts and custom_watchdog.ts where the package's other non-plugin
modules live. The spec already imported the module by relative path and
already ran in browser mode, so it needed no change and slots straight
into the aggregate's runner and its 100% coverage gate. The one-line
barrel is dropped; both importers now reference the module directly.
`bootstrap` moves across as a peerDependency of the aggregate, which now
imports Tooltip directly. It resolves from the hoisted root either way,
but the dependency is real and should be declared.
Also unwires the package from the root test:parallel/test:sequential
filters, the three dedicated CI steps in dev.yml, and the Codecov flag —
its coverage now reports under the ckeditor5 flag.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Codecov wiring landed on main after this branch was written, and the
rebase applied cleanly without reconciling with it.
Emit repo-root-relative lcov paths, as every other suite now does:
codecov.yml has dropped its `fixes:` block on the basis that each
project pins the reporter's `projectRoot`, and package-relative `SF:`
paths are ambiguous across the monorepo — coverage lands on whichever
package wins the match rather than this one.
Gate the uploads on the test step's outcome instead of `always()`, and
set `disable_search`, matching the surrounding steps.
Co-Authored-By: Claude <noreply@anthropic.com>
The package's tests were already gating the build, but only via the
catch-all step at the end of the job — an exclusion list that never
named it. That step runs without `--coverage` and uploads nothing, and
codecov.yml had no flag covering packages/pdfjs-viewer, so none of the
suite was visible in Codecov.
Give it the same treatment as its sibling packages: a dedicated step
that collects coverage, the two Codecov uploads, and a flag. The
catch-all now excludes it so it does not run twice, and the config gains
the junit reporter the test-results upload needs.
Co-Authored-By: Claude <noreply@anthropic.com>
The test steps run sequentially in one job with no `if:`, so when one fails GitHub skips the
rest — but every Codecov upload step is `if: always()`. The named lcov then does not exist, and
because `disable_search` defaults to false the CLI falls back to scanning the repo and uploads
whatever it finds. With `fail_ci_if_error: false` this is reported as a successful upload.
Seen on this PR when the server suite hit its teardown flake:
warning -- Some files were not found --- {"not_found_files": ["packages/highlightjs/.../lcov.info"]}
info -- Found 4 coverage files to report
> apps/server/test-output/vitest/coverage/lcov.info
> apps/client/test-output/vitest/coverage/lcov.info
> apps/server/test-output/vitest/html/coverage/lcov.info
> apps/client/test-output/vitest/html/coverage/lcov.info
info -- Sending upload (1304438 bytes) to storage
So the `highlightjs` flag received the client+server report. Filtered to that flag's path it
kept only the three highlightjs files the client happens to load, and the flag read 4.11%
against a real 100% — with the `languages/*.ts` files missing entirely because the client never
loads them. The junit uploads were poisoned the same way. Any early failure could do this to up
to seven package flags at once, which combined with `carryforward: true` is a second, previously
unnoticed source of wrong coverage numbers.
Set `disable_search: true` on all twenty upload steps so a missing report fails loudly instead of
silently substituting someone else's, and gate each on its own test step. Coverage uploads require
that step to have succeeded, since a failed suite's coverage is incomplete; test-result uploads
only require that it ran, so a failing suite still reports which tests failed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The catch-all test step runs without --coverage and has no upload, so the three suites just
wired up produced no coverage signal. Give each the same treatment commons already gets: a
dedicated --coverage run plus coverage and test-results uploads, and exclude them from the
catch-all so they don't run twice.
Each package's vitest config gains the junit reporter the test-results action needs, and
express-partial-content also gains the lcov coverage reporter it was missing. The emitted
paths are package-relative (SF:src/...), matching commons, whose upload already resolves
against packages/commons/src/ — so no codecov.yml path fixes are needed.
Also declares the ckeditor5-utils flag, which the workflow has been uploading all along
without a matching entry, leaving it without paths scoping or carryforward.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The ckeditor5-utils package runs its tests in WebdriverIO + headless
Chrome, but the CI workflow never registered it as a browser-mode
suite. It fell into the catch-all "Run the rest of the tests" step,
which has no CHROMEDRIVER_PATH exported, so WebdriverIO tried to
auto-download chromedriver into /tmp and failed on a partial cache.
Give it a dedicated step that exports CHROMEDRIVER_PATH (like the
mermaid/math/aggregate suites), run it with --coverage, upload the
coverage and test results to Codecov under a ckeditor5-utils flag, and
exclude it from the catch-all step.
Enabling --coverage surfaced a real gap (98.19% vs the package's 100%
threshold): the function-form customClass path in _customClass() was
untested. Add a test covering it so the suite lands at 100%.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CI never produced coverage for packages/ckeditor5 (it ran in the catch-all
"rest of the tests" step without --coverage), and the codecov-action steps
upload only explicit hardcoded paths — so the package never reached Codecov.
- dev.yml: run the ckeditor5 browser-mode suite with --coverage in its own
step (sharing the chromedriver setup the other CKEditor tests use), upload
the lcov + junit results to Codecov under a new `ckeditor5` flag, and drop
ckeditor5 from the no-coverage catch-all so it isn't run twice.
- vitest.config.ts: emit junit results and write coverage to
test-output/vitest/coverage (matches the other suites' convention and lands
under the already-gitignored test-output/ instead of an untracked coverage/).
- codecov.yml: add the `ckeditor5` flag scoped to packages/ckeditor5/src/.
This also makes CI enforce the package's 100% coverage gate (the threshold
already lived in vitest.config.ts but was never exercised under --coverage).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
WebDriverIO downloads chromedriver at test time, and this download
intermittently hangs on CI runners causing a 60s+ timeout. Setting
CHROMEDRIVER_PATH to the runner's pre-installed chromedriver skips
the download entirely.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>