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 website picked its languages from a hand-maintained `LOCALES` array that
nothing checked, so it had fallen behind the `website` Weblate component. Thirteen
languages above the 50% bar the client uses were bundled but unreachable, German,
Indonesian, Korean and Irish among them at 100%, while Romanian at 54.9% was
offered. Add them, reusing the endonyms already curated in `packages/commons`.
`mapLocale()` ended in `locale.split('-')[0]`, which cannot express a locale that
carries a region: `en-GB`, `pt-BR` and `nb-NO` would have collapsed to `en`, a 5.1%
`pt` and a nonexistent `nb`. Resolve against `LOCALES` instead, preferring an exact
match, then the region-less entry for the language, then any region sharing it. A
bare `nb` or `pt` now reaches `nb-NO` or `pt-BR`, and a language the website does
not offer returns `en` rather than a locale with no entry in the picker. The
region-less step matters: `LOCALES` is sorted by display name, so without it
`en-US` resolves to `en-GB`.
`check-translation-coverage.ts` now gates the `website` component alongside
`client`, so the next language to cross the threshold fails CI instead of sitting
unreachable. It reports every missing locale rather than the first, and names the
file to add it to.
`LOCALES` moves to `apps/website/src/locales.ts` because the gate runs under tsx,
which cannot evaluate the `import.meta.glob` at the top of `i18n.ts`.
`en` is renamed to "English (United States)", matching `packages/commons`, since
"English" alongside "English (United Kingdom)" names nothing in particular.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Plaintext size occupies bytes 32-39, so a reader that grabs only the
first 32 bytes to identify a container cannot see it. The fixed header
is FIXED_HEADER_BYTES = 40 throughout. The same figure was wrong in
decodeFixedHeader's own doc comment.
Also drop a stray template artifact in the trailer section.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The KDF-parameter table named bytes 33 to 35, contradicting both the
field table above it and `decodeHeader()`, which reads log2N, r and p
from offsets 41, 42 and 43. The extension notes put the authenticated
header at 60 bytes; `authenticatedHeaderEnd()` returns headerLength -
TAG_BYTES, so version 1 authenticates 68.
The Developer Guide index also carried a `[missing note]` placeholder
from the docs export in place of the Project Structure link label.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three findings that had been living outside the repository, each
re-verified against the current source before writing.
Concepts/Synchronisation/Entity change tombstones: erased entities are
flagged rather than removed, and nothing ever collects them, so
entity_changes grows with lifetime deletion activity instead of live
data. Explains why they cannot simply be deleted — content_hash folds
isErased into the per-sector hash, so pruning on one instance diverges
it from peers, which then re-push the tombstones back — why
fillEntityChanges and forceFullSync both make it worse, the conditions
under which a manual delete does stick, and the existing blob purge as
the one special case that was never generalised. Sits next to Content
hashing, which is the mechanism it depends on.
Dependencies/Compression libraries: why three ZIP implementations
coexist. exceljs hard-depends on jszip for the XLSX container, fflate is
the deliberately lighter browser choice, and the Node providers use
archiver + yauzl. Also records that commons is browser-pure and has been
getting the Buffer global type from jszip's node type reference, which
breaks if jszip ever leaves.
Troubleshooting/A note is listed in Recent Changes but missing from the
tree: entity-change propagation is in-process only, so two processes on
one data directory each hold a Becca that never learns of the other's
writes. Recent Changes reads raw SQL and sees the note, the tree reads
Becca and does not, and the deleted-note metadata route then throws
NotFoundError because it filters on isDeleted = 1 while the row is live.
Reads as cache corruption and is not.
New docs are registered in !!!meta.json, which enumerates every file in
the guide.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Merge the app list into the monorepo tree, fold the do-not-run rules and
the TypeScript-versions note under Development Commands, compress
platform/core rules, i18n, code style and the recipes to their rules,
and point API routes, migrations and translations at the skills that now
own them. The full TypeScript-versions reasoning moves to the developer
guide's Environment Setup page. Key Entry Points and Build System Notes
are dropped as redundant.
Co-Authored-By: Claude Fable 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>
Both plugins win by listening at a higher priority than ImageUploadEditing, so
the code they pre-empt stays in place and reads correctly on its own — each is
easy to mistake for redundant. Write down what they displace and what breaks if
the narrowness goes: widening the bare-image rule brings back the Word paste
that loses its text, and dropping the restore priority makes every internal
paste duplicate its images.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
WebdriverIO downloads a Chrome for Testing build and a chromedriver of
its own, both linked against libraries no NixOS store path provides, so
they abort on a missing libxcb.so.1 and the suite cannot start. Working
around it by hand — a driver started on a fixed port, a local config
connecting to it — is a recipe everyone has to rediscover.
Two variables replace it. CHROMEDRIVER_PATH is webdriverio's own, and
makes it spawn that driver on a free port; CHROME_BIN is read here and
handed over as a capability, which also stops the browser download,
since a string binary short-circuits the puppeteer setup. Neither is set
on a normal machine, so nothing about CI changes.
The dev shell exports both from pkgs.chromium and pkgs.chromedriver —
one nixpkgs revision, so their versions agree.
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-footnotes` had no consumers outside
`@triliumnext/ckeditor5`, was never published, and shipped a
ckeditor5-package-generator scaffold (sample pages, its own ESLint /
Stylelint / vitest configs, typings) that no root script or CI job ever
invoked. Its vitest config declared a 100% coverage gate while the
package contained no tests at all, and CI never ran it with
`--coverage`, so ~1300 lines went entirely unexercised.
Move it to src/plugins/footnotes/, flattening the footnote-editing/
subdirectory and renaming to the repo's snake_case convention, and adapt
it to the destination: repo code style, the augmentation folded into a
`declare module` block, and the theme CSS moved to src/theme/. The ISC
license and its Forum Magnum / Bohan Niu attribution move with the code.
Add the missing tests — 77 across the command, editing, UI, converters,
schema, autoformat and utils — bringing the plugin to 100% statements,
functions and lines inside the aggregate's gate. Fixtures build the
document with a model writer rather than `_setModelData()`, which
coerces numeric-looking attribute values to numbers and reads `[^1]` as
selection markers; both silently made earlier fixtures test nothing.
Fix a renumbering bug found while writing those tests: `_removeFootnote`
used `${ index ?? 0 + i + 1 }`, which parses as `index ?? (0 + i + 1)`.
Since `index` is non-null there, every trailing footnote was assigned
the same number rather than being renumbered in sequence.
Also drop code that had no reachable path: the `modelQueryText` and
`modelQueryTextAll` helpers and the `DATA_FOOTNOTE_ID` constant (no
callers), the schema's `listItem` child check (the list feature has
modelled entries as attributed blocks since v41, so no `listItem`
element exists), an `editor.plugins.has("Autoformat")` guard inside a
plugin that `requires` Autoformat, and a handful of duplicated or
impossible null checks. The remaining defensive guards are marked with
`/* v8 ignore next -- defensive: ... */`, matching the convention
already used elsewhere in the repo, rather than deleted from ported
third-party code.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`packages/ckeditor5-admonition` had no consumers outside
`@triliumnext/ckeditor5`, was never published, and shipped a
ckeditor5-package-generator scaffold (sample pages, its own ESLint /
Stylelint / vitest configs, typings) that no root script or CI job ever
invoked. Half the feature already lived in the aggregate:
admonition_toolbar.ts and admonition_type_dropdown.ts were here while
the plugin they drive was in the package.
Move it to src/plugins/admonition/ alongside those two, renamed to match
the repo's snake_case convention, and adapt it to the destination: repo
code style, the augmentation folded into a `declare module` block, the
`(this as any)` cast in the autoformat replaced by the real Autoformat
instance, and three non-null assertions removed.
Two latent problems are straightened out on the way:
- The package exported two different `ADMONITION_TYPES` — a tuple of
names from the command module and a Record<type, {title}> from the UI
module — and the barrel re-exported the UI one, shadowing the other.
The tuple is now ADMONITION_TYPE_NAMES.
- theme/blockquote.css is not admonition styling at all; it is the
baseline `blockquote` CSS inherited from the block-quote fork. It
moves to src/theme/blockquote.css and is imported from src/index.ts
with the other global stylesheets.
The feature arrived with zero tests, so add specs for the command,
editing, UI and autoformat parts (47 tests). Reaching the aggregate's
100% coverage gate required removing three genuinely unreachable
fragments rather than faking coverage for them:
- the post-fixer branch unwrapping an `<aside>` inserted at an illegal
position — writer.insert()/move() both normalise it out of such a
parent first, so the differ never reports one there;
- two `match.length` guards in the autoformat, on a regex with exactly
one capture group;
- two `is("element")` re-checks on findQuote(), which can only ever
return an element.
Suite: 891 tests, 100% statements/branches/functions/lines.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`packages/ckeditor5-keyboard-marker` had no consumers outside
`@triliumnext/ckeditor5`, was never published, and carried a full
ckeditor5-package-generator scaffold (sample pages, its own ESLint /
Stylelint / vitest configs, typings) that no root script or CI job ever
invoked. Its GPL-3.0 license combines with this repository's
AGPL-3.0-only, so the split bought no license isolation either, and
upstream (mlewand/ckeditor5-keyboard-marker) targets a long-obsolete
CKEditor 5 version and is not tracked — the source was ported and has
since diverged.
Move the plugin to src/plugins/keyboard_marker/, renamed to match the
todo_list_multistate convention, and adapt it to the destination
package: repo code style, the augmentation folded into a `declare
module` block, and the spec moved onto the shared createTestEditor()
kit. The unused `icons` barrel export is dropped.
Attribution is preserved: the upstream LICENSE moves next to the code,
alongside a README recording the provenance and the divergence.
The plugin's tests land inside the aggregate's 100% coverage gate
without a carve-out; the suite stays at 100% (848 tests).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Registers ko in UNSORTED_LOCALES and maps it through every locale
registry keyed by DISPLAYABLE_LOCALE_IDS: dayjs, FullCalendar,
Excalidraw (ko-KR), Univer (KO_KR, all nine preset bundles) and
CKEditor. PDF.js needs no mapping — ko resolves through electronLocale
and the viewer already ships ko/viewer.ftl.
The CKEditor test pins every locale by hand rather than iterating them,
so it takes an entry of its own to stay complete.
The developer guide was missing the Univer step altogether, so record
that, along with the CKEditor test caveat, the condition under which
PDF.js needs nothing, and the fact that typecheck enumerates the rest
once the locale is registered.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`edit-integration-db` wrote to `apps/server/spec/db/document.db`, which is
git-ignored, while the DB the tests actually load is the tracked fixture at
`packages/trilium-core/src/test/fixtures/document.db`. Editing the DB therefore
required a manual copy dance in and out of the fixture.
Point the tool at the tracked fixture via `TRILIUM_DOCUMENT_PATH` (keeping
`TRILIUM_DATA_DIR=spec/db` so logs/tmp/config stay git-ignored), and open the
DB in the rollback (DELETE) journal mode under `TRILIUM_INTEGRATION_TEST` so the
fixture stays a single committable file with no WAL sidecars. Update the (stale)
Test database developer doc to match.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `docs/Script API/` directory was an orphaned TypeDoc snapshot from an
older docs pipeline. The current generator (apps/build-docs) builds the
Script API into the gitignored `site/script-api/{backend,frontend,electron}`
and publishes it to docs.triliumnotes.org, so the committed copy was no
longer regenerated and had gone stale (its media/README*.md landing pages
still referenced trilium.cc and the old wiki URLs).
Delete the folder and update the docs references that still pointed at it
(CLAUDE.md, copilot-instructions.md, Developer Guide/Documentation.md).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>