## Linked issue (required)
Fixes#5510
## Summary / motivation (required)
Expands the manual translation instructions as requested in #5510:
- `docs-site/translators/anki/manual.mdx` now contains detailed
instructions for translating the documentation: forking the repository,
working in a language subdirectory (existing or new), keeping Markdown
formatting intact while translating the MDX pages, registering
translated pages in `docs.json` (with an example following the shape of
the existing language entries), and submitting a PR. It also notes that
partial translations are welcome, that documentation changes are
licensed under CC BY-SA 4.0, and where to ask questions.
- The Translations section of `docs-site/README.md` now links to that
page instead of holding the details itself, reversing the previous
direction per the issue.
- The Translations sections of `docs-site/developers/contributing.mdx`
and the cog-generated `docs/contributing.md` briefly mention
documentation translations, which were previously only covered for
interface strings.
Also adds myself to the CONTRIBUTORS file in a separate commit, as
requested for first pull requests.
## Steps to reproduce (required, use N/A if not applicable)
N/A
## How to test (required)
Documentation-only change. Verified that:
- All paths referenced in the new instructions exist in the repository
(e.g. `docs-site/ru/manual/`, `docs-site/manual/`, and the other
language subdirectories).
- The `docs.json` example matches the structure of the existing language
entries under `navigation.languages`.
- Both internal links resolve to pages registered in
`docs-site/docs.json` (`/translators/anki/manual`,
`/translators/anki/developers`).
- No MDX-special characters (`{`, `<`) appear outside fenced code blocks
in the new content, so `mintlify validate` should pass.
- `docs/contributing.md` stays in sync with
`docs-site/developers/contributing.mdx` under the transformation in
`docs/cogdocs.py`.
### Checklist (minimum)
- [ ] I ran `./ninja check` or an equivalent relevant check locally.
*Not applicable for a documentation-only change; the relevant check is
the Docs Site workflow (`mintlify validate` / `mintlify a11y`), which
runs in CI.*
- [ ] I added or updated tests when the change is non-trivial or
behavior changed.
*N/A - documentation-only changes are exempt from tests per
`contributing.md`.*
## Before / after behavior (optional)
N/A
## UI evidence (required for visual changes; otherwise N/A)
N/A
## Scope
- [x] This PR is focused on one change (no unrelated edits).
---------
Co-authored-by: Abdo <abdo@abdnh.net>
## Linked issue
Fixes#5657
## Summary / motivation
SonarCloud PR analysis derives the changed-file set from git and
requires the target branch to be present in the local repository. The
scan checks out only the fork's head history (`fetch-depth: 0` on the
head ref), so the base branch was never fetched. Without a base to diff
against, the scanner attributed every analysed file to the PR as new
code, e.g. 977 files on the single-file change in #5641
(`qt/aqt/preferences.py`), and recurring noise in `tools/`.
This adds a step that fetches the target branch from the trusted
upstream repository (`github.repository`, never the fork) and pins it
locally under the name passed as `sonar.pullrequest.base`, so new code
is measured against the real PR diff. It does not disable any rule or
exclude any file.
The isolation boundary is preserved: the upstream branch is read as data
only, nothing is built or executed, the trusted config and
no-fork-code-execution constraints are unchanged, and the Quality Gate
stays informative.
## Steps to reproduce
1. Open a fork PR that changes a single file (e.g. #5641, base `main`).
2. Let CI complete and the SonarCloud workflow run.
3. Observe SonarCloud reporting new-code issues across hundreds of
unchanged files (~977) instead of only the changed file.
## How to test
### Details
Reproduced the scanner's git-based new-code computation locally against
the exact commits from #5641:
- head = `1e7914a`
- base branch `main` fetched from upstream → `merge-base` = `bdeec0877`,
which matches the PR's `baseRefOid` reported by the GitHub API.
- `git diff --name-only <merge-base> <head>` → **only**
`qt/aqt/preferences.py`.
The new step also logs the analysed commit, base repo/branch, base
commit and merge-base, and fails closed if the base reference cannot be
resolved (no silent fallback to a missing base). YAML parse and the
cog-generated `docs/sonarcloud.md` sync were verified.
## Before / after behavior
- Before: fork/PR scan treats all indexed files as new code (977 files
for a
one-file PR).
- After: only the files actually changed vs the upstream base are
considered
new code.
closes#5652
`col.fsrs_enabled` should be usable in any place where we have access to
the collection (And that doesn't involve building the queue or modifying
the config).
c.c. @user1823
Unfortunately the change in #5626 to skip the scope dialog in the MSI
installer depending on the detected installation does not work as
expected in some cases. To reproduce:
- Install 26.09 on all-users scope.
- Install 26.09.2 on all-users scope.
- Bump .version (26.09.3) and build a new installer with #5626
- Install 26.09.3 and notice the default installation path is
`C:\Users\%USERNAME%\AppData\Local\Programs\Anki`, despite there being
two all-users installations.
Reverting for now. We can revisit it for the next major release.
fixes#5635
We could fix specifically the SM2 part of this bug with just the
"fsrs_enabled" part; but then if we encounter a card with the memory
state actually missing while reviewing I think we would get the same
error. So now any calculated memory state is only shown in the card info
and not writ to the database after being calculated.
## What was happening
When I set up the Sonar PoC, I pinned `sonar.projectVersion` to `0.1`.
The New Code period is "previous version", so with the version never
changing, the baseline stayed frozen at that first analysis. Everything
merged since then kept accumulating as "new code" instead of rolling off
at each release, which is why the Quality Gate on `main` has been red
since August (e.g., coverage for "new code" is at 62%, below the 80%
minimum).
## The fix
Now the Sonar version follows the project version instead of being
pinned, so every release bump gives us a fresh baseline. Pulled from the
trusted default-branch checkout so a fork can't tamper with it.
## How it works from now on
PRs won't change: Sonar already checks only your diff, so the 80%
coverage gate applies just to the lines you touched, not the old debt.
For `main`: the baseline now resets on each release. Just note it moves
on the next version bump, not on this merge, so `main` stays red until
then, and after that "new code" is only what changed since the last
release.
Closes#5629
I decided to do this by moving safe mode to "app" rather than by moving
the rendering code to the main window
(2db70434ea1412b4513479236cd74464de986b6a)
If 2db70434ea1412b4513479236cd74464de986b6a is preferred then I'll
switch back to it
## Linked issue
Closes#5037
## Summary
The MSI installer fails to detect existing installations and results in
multiple versions being installed. This was fixed in #5038 but
apparently later template changes reintroduced the issue (Windows
Installer is full of footguns and is very fussy about ordering...).
## Steps to reproduce (before)
- Install 26.09 in all-users scope.
- Install 26.09.2 in all-users scope and confirm it results in two
entries in _Settings > Apps > Installed Apps_.
## How to test (after)
Repeat the same process. All-users scope should now by selected by
default if an existing installation is detected and replaces the
existing one correctly.
AnkiWebPage.acceptNavigationRequest() did not check the full host and
port before accepting navigation requests, which made it possible for
external URLs to be opened in a trusted webview with access to pycmd().
This also adds a port check to AuthInterceptor so the API key is not
sent to a localhost server on a different port.
_This fix is already included in 26.09.1_.
## Linked issue
Closes#5387
## Summary / motivation
Add tests for the `TagsService` protobuf layer, covering all service
endpoints, request conversions, persisted changes, and representative
error paths.
This completes the remaining tags service coverage requested in #5387.
The media service portion was previously covered by #5588.
## How to test
### Details
Ran:
- `just test-rust`
- `just test-rust --coverage --html`
All Rust tests pass. Production code in `rslib/src/tags/service.rs` has
80/80 lines covered (100%).
<img width="1143" height="24" alt="image"
src="https://github.com/user-attachments/assets/fc8dd1d7-4903-4528-a92d-f985d7fafe7f"
/>
## Before / after behavior (optional)
Before: `rslib/src/tags/service.rs` had no direct test coverage.
After: all `TagsService` endpoints are covered, including persisted
mutations, protobuf request handling, empty-ID behavior, tag-name
canonification, and explicit error variants.
## Linked issue
Closes#5384
## Summary / motivation
`rslib/src/card_rendering/service.rs` had 0% test coverage, whereas the
issue asks for at least 70%.
This PR adds golden unit tests that drive the `CardRenderingService`
trait implementation directly through qualified trait syntax, so that
the service layer itself gets exercised (its proto conversion,
validation and mode/flag dispatch) rather than the inherent `Collection`
render methods it delegates to.
Coverage, grouped by the issue's areas:
- **Question/answer rendering**: `render_uncommitted_card` /
`render_existing_card` produce the expected nodes, css and `is_empty`.
- **Template filters**: a known filter is applied; an unknown filter is
ignored when `partial_render` is false and emitted as a replacement node
when true, including when preceded by literal text.
- **Edge cases**: empty fields report `is_empty`; cloze rendering; the
empty-card report for both a note kept alive and a note marked for
deletion.
- **Error handling**: missing template/note and unknown card id return
the concrete `InvalidInput` variant and message; invalid legacy template
bytes return `JsonError`, so that none of these paths panic.
- **Delegating methods**: av-tag/latex extraction, markdown
(with/without sanitize), IRI path round-trip, strip-html modes,
html-to-text, compare-answer, cloze-for-typing, and the legacy render
path.
No production code was changed, since this PR adds tests only.
## How to test
### Details
- `cargo test -p anki card_rendering::service` → 27 passed.
- `just test-rust` → full Rust suite green; `cargo fmt --check` and
`clippy` clean on the changed file.
- `just test-rust --coverage` → `service.rs` now at 100% function and
~99% line coverage; the only uncovered lines are test-only guard arms
(`_ => None` fallbacks and `other => panic!(...)` in error matches),
which execute only on test failure and are intentionally left uncovered
rather than padded.
- Note: the repository-wide `just check` also runs the web/Python
stacks; this change is Rust-test-only, so the relevant Rust checks above
are the ones that apply.
<img width="994" height="28" alt="image"
src="https://github.com/user-attachments/assets/67479d89-66bc-4a2e-bdbd-fe16a0da3114"
/>
closes#5598
I tried to use a glob but it didn't like redirecting based on it ending
in .html 😢
```json
{
"source": "/:glob.html",
"destination": "/manual/:glob"
}
```
```json
{
"source": "/:glob*.html",
"destination": "/manual/:glob*"
}
```
Does not work. So I brute forced it.
## Linked issue
Closes#5603
## Summary
Briefcase recently started to adding the "lib" subfolder to PYTHONPATH
(https://github.com/beeware/briefcase-windows-VisualStudio-template/pull/105).
This unfortunately caused unexpected issues with users trying to upgrade
to 26.09 who happened to have leftover lib/anki/_rsbridge.pyd libraries
(probably resulting from failed uninstalls) from PyOxidizer-based
pre-25.07 builds.
The fix suggested here is to revert the upstream change. This involves
switching from the pre-built
[briefcase-windows-app-template](https://github.com/ankitects/briefcase-windows-app-template)
template to
[briefcase-windows-VisualStudio-template](https://github.com/ankitects/briefcase-windows-VisualStudio-template).
I made sure to port all Anki-specific changes we made to the Wix
template (such as uninstall handling for the old NSIS-based installer).
## Steps to reproduce (before)
- Install 26.09
- Download and extract a lib folder from a PyOxidizer-build such as
[25.02.7](https://github.com/ankitects/anki/releases/tag/25.02.7) (You
can unzip the .exe using [7-zip](https://www.7-zip.org/)).
- Move the lib folder to the installation folder of 26.09
- Run Anki and confirm you get an error.
## How to test (after)
Repeat the same process but with this PR (`./tools/ninja installer`) and
confirm the lib subfolder has no effect.
The deprecation warnings added in #5547 incorrectly mention
_LegacyAnkiPackageExporter/_LegacyAnkiPackageImporter as the replacement
APIs instead of
Collection.export_anki_package/Collection.import_anki_package.
_This fix is already included in 26.09_
## Linked issue
Closes#5592
## Summary
Our Briefcase build didn't respect locked dependencies and dependency
cooldowns and used to resolve newer versions of some packages such as
`click` and `protobuf`.
This PR fixes the issue by exporting a list of pinned dependencies to a
file and passing it to uv using the `--constraints` option.
## Steps to reproduce (before)
- Build the bundle: `./ninja installer:build`.
- Inspect `out/installer/build/anki/*/app_packages` and notice
`protobuf-7.36.1.dist-info` is included despite it being locked to
6.33.5 in uv.lock
## How to test (after)
Build the bundle again and confirm there's no mismatch between locked
and installed dependency versions.
## Linked issue
Follow-up to #5498, which was closed in favor of a smaller and more
maintainable implementation.
## Summary / motivation
Run CI-based SonarCloud analysis for internal and fork pull requests
while keeping the Quality Gate informational.
The analysis matrix is:
| Event | Static analysis | Coverage in CI | Coverage in SonarCloud |
| --- | --- | --- | --- |
| Internal pull request | Yes | Yes | Yes |
| Push to `main` or `release/**` | Yes | Yes | Yes |
| Fork pull request | Yes | Yes | No |
Fork pull requests continue to run the full test coverage and
coverage-regression checks in the unprivileged CI workflow. Their
contributor-produced coverage reports are not passed to the privileged
SonarCloud workflow.
The fork scan reports static bugs, vulnerabilities, security hotspots,
and code smells. It:
- validates the PR state, repositories, branches, and tested SHA against
the GitHub API;
- checks out the exact revision tested by CI;
- replaces `sonar-project.properties` with the trusted default-branch
version;
- provides `SONAR_TOKEN` only to the scanner step;
- never builds, installs, or executes contributor code;
- keeps scanner-side Clippy and SCA disabled;
- clears coverage report paths and excludes coverage calculation for
forks.
Automatic Analysis remains disabled because it cannot be combined with
CI-based analysis and does not provide the required Rust and coverage
support.
This also configures Vitest to produce repository-relative LCOV paths
such as `SF:ts/lib/...`, allowing SonarCloud to resolve TypeScript files
when reports are downloaded into a separate checkout.
## How to test
### Checklist
- [x] I ran `just check` or an equivalent relevant check locally.
- [ ] I added or updated tests when the change is non-trivial or
behavior changed. The remaining behavior is specific to GitHub's
`workflow_run` environment and can only be exercised end-to-end after
the workflow is available on the default branch.
### Details
The following checks passed locally:
- `just test-ts --coverage`
- `just test-ts --coverage --html`
- `just fmt`
- `just check`
- YAML and JSON parsing
- `git diff --check`
Both TypeScript coverage runs passed all 67 tests. The generated LCOV
report was also checked to confirm that source paths are
repository-relative and begin with `SF:ts/`.
After this lands, end-to-end validation should cover:
1. A push to `main`, confirming that Python, TypeScript, and Rust
coverage reports are imported.
2. An internal PR, confirming static analysis and coverage.
3. A fork PR, confirming static analysis while the coverage download is
skipped and no coverage condition is evaluated.
## Before / after behavior
Before:
- fork PR analysis fails at the protected checkout;
- the `actions/checkout` version comments are outdated;
- TypeScript LCOV paths are relative to `ts/` and may not resolve in the
SonarCloud checkout.
After:
- internal PRs and trusted branch pushes receive static analysis and
coverage;
- fork PRs receive static analysis without importing
contributor-produced coverage;
- fork tests and coverage-regression checks continue to run in regular
CI;
- TypeScript LCOV paths resolve from the repository root;
- technical scanner failures remain visible without making the Sonar
Quality Gate a required merge check.
## Risk / compatibility / migration
The fork analysis runs in a privileged `workflow_run` and necessarily
asks the SonarScanner to parse untrusted source while it has access to a
project-scoped token.
The workflow limits this exposure by using GitHub-hosted runners,
read-only permissions, trusted scanner configuration, an exact tested
SHA, API-validated PR metadata, and no builds, dependency installation,
local Actions, caches, or fork-produced coverage artifacts.
This is intentionally a smaller trust boundary than the data-only
snapshot and custom coverage parser proposed in #5498. It prevents
direct execution of contributor code but does not eliminate potential
vulnerabilities in GitHub Actions or Sonar analyzers.
Automatic Analysis must remain disabled, `SONAR_TOKEN` should remain
restricted to this project, and the SonarCloud check must remain
non-required.
## Linked issue (required)
Fixes#5342
## Summary / motivation (required)
When installing an add-on from AnkiWeb, Anki currently derives the
displayed
name from the AnkiWeb download filename and passes it as overriding
manifest
metadata. This can alter names when the filename has been sanitized by
AnkiWeb.
This change prefers a non-empty `name` from a valid packaged
`manifest.json`
when available, while retaining the existing AnkiWeb-derived name as a
fallback.
## Steps to reproduce (required, use N/A if not applicable)
1. Package an add-on with a `manifest.json` whose `name` contains a
character
that is removed from the AnkiWeb download filename, such as `&` or `𝕾`.
2. Upload the add-on to AnkiWeb and install it from there.
3. Open the Add-ons dialog and observe that the displayed name is
derived from
the sanitized AnkiWeb filename rather than the packaged `name`.
## How to test (required)
### Checklist (minimum)
- [x] I ran `./ninja check` or an equivalent relevant check locally.
- [x] I added or updated tests when the change is non-trivial or
behavior changed.
### Details
Added tests verifying that:
- a non-empty `name` from a valid packaged manifest is preferred for
AnkiWeb
installs;
- the existing filename-derived name remains the fallback when no usable
packaged name is available.
`./ninja check` passes locally.
Also manually verified with a local build by installing an AnkiWeb
add-on whose
packaged name differed from its AnkiWeb title; the Add-ons dialog used
the packaged name.
## Before / after behavior (optional)
Before: AnkiWeb installs always use the filename-derived name,
overriding a
`name` supplied in the packaged manifest.
After: A non-empty packaged `name` is preferred when available;
otherwise,
the existing AnkiWeb-derived name is retained.
## Risk / compatibility / migration (optional)
Low. The change is limited to AnkiWeb add-on installation, and the
existing
fallback behavior is preserved for add-ons without a usable packaged
name.
## UI evidence (required for visual changes; otherwise N/A)
N/A
## Scope
- [x] This PR is focused on one change (no unrelated edits).
## Linked issue (required)
Fixes#5567
## Summary / motivation (required)
Pasting a base64 image in the new editor currently corrupts its bytes,
and the HTML paste path can put image markup into an image's `src`.
Decode base64 directly into bytes and return the saved filename from the
inline-image helper. Read the clipboard PNG preference's boolean value
so a disabled preference selects JPEG.
Add regressions through the public clipboard APIs for plain-text and
HTML data URLs and both image-format settings. Include editor route
sources and tests in the Vitest build inputs so edits to these
regressions reliably rerun the tests.
## Steps to reproduce (required, use N/A if not applicable)
Using a disposable profile on the base revision:
1. Open the new editor and disable "Paste without shift key strips
formatting" to enable extended paste.
2. Paste the base64 PNG data URL from the linked issue as plain text,
then as the source of an image in clipboard HTML. Inspect the stored
media bytes and inserted image source: the bytes change and the HTML
source is invalid.
3. Disable "Paste clipboard images as PNG" and paste a bitmap image
through the clipboard path. PNG is still selected.
4. After a successful `just test-ts`, edit only an editor route test and
rerun it. The build incorrectly reuses the prior Vitest result.
## How to test
- [x] `RELEASE=1 just check` passed on this focused upstream-based
branch (`ee16fc015`), using Apple Silicon macOS and the repository's
pinned toolchains.
- [x] Added or updated regression coverage for the changed behavior.
Clipboard regressions check exact decoded bytes, a local media filename
in the inserted image, and both PNG/JPEG preference values. Confirmed
route test edits invalidate the Vitest target. Clipboard/backend APIs
are mocked; no user collection is accessed.
## Risk / compatibility / migration (optional)
No database migration or new dependency. The legacy paste branch is
unchanged. The build input change only causes relevant source/test edits
to rerun Vitest. Complete local validation was on macOS; other platforms
depend on CI.
## UI evidence (required for visual changes; otherwise N/A)
N/A: no visual design changes. Regression tests inspect the inserted
image markup.
## Scope
- [x] This PR is focused on one change (no unrelated edits).
---------
Co-authored-by: Abdo <abdo@abdnh.net>
## Linked issue
Fixes#5336
## Summary / motivation
The Windows installation guide lists only a generic 64-bit Windows
requirement, even though Anki provides separate x64 and ARM64 installers
with different Windows requirements. Document both configurations and
explain how to match the processor type shown in Windows Settings to the
download label.
## Steps to reproduce
N/A: documentation only.
## How to test
- [x] `RELEASE=1 just check` passed on the focused branch on macOS Apple
Silicon.
- [x] No unit tests added for a documentation-only change.
- Confirmed the requirements and download labels against [Anki's
download page](https://apps.ankiweb.net/): Windows 10+ (x64) and Windows
11 (ARM).
- Confirmed that release 26.08.1 includes separate `win-x64.msi` and
`win-arm64.msi` installers.
- Checked the Settings path against [Microsoft's system-type
instructions](https://support.microsoft.com/en-us/windows/32-bit-and-64-bit-windows-frequently-asked-questions-c6ca9541-8dce-4d48-0415-94a3faa2e13d)
and [Windows ARM64
guidance](https://www.microsoft.com/en-us/software-download/windows11arm64).
- Mintlify 4.2.884: build validation and accessibility checks passed.
The site-wide accessibility scan reports 32 pre-existing parse warnings
on other pages; the edited Windows page is checked successfully.
## Risk / compatibility / migration
Documentation only. Existing guidance for older Windows releases and
32-bit systems is retained. No installer or platform support changes.
## UI evidence
N/A: documentation only.
## Scope
- [x] Focused on Windows requirements and choosing the matching
installer.
---------
Co-authored-by: Abdo <abdo@abdnh.net>
## Linked issue (required)
Closes#5561
## Summary / motivation (required)
Importing `./index` was taking a long time which timed out several
tests, and the scripts mock wasn't reset between tests. I added a dummy
import beforehand to warm the cache but test timeouts might still need
increasing
closes#5064
Should let you quickly import to mintlify from the repos that use the
existing format.
a small problem that `DOCS_RELATIVE_LINK_REPLACEMENTS` needs to be set
manually for links between related translations but other than that
should be good.
I've run it on `en/addons` `en/faqs` `en/manual` and on `ar/manual`
`ar/faqs`. We might need to be careful to not overwrite any changes that
have been merged in this repo if there are any.
I've set it to copy only files where it can find them in the existing
mintlify docs because this allows us to more easily keep the site
structure. It does mean that there are some pages which aren't copied
and I'm going to have to look into that at some point.
But it should be good enough to look through and to import any remaining
changes left in the existing repos.
Example usage:
```
python tools/docs-converter.py [Source repo] [Target tab] [Target language code]
```
```
python tools/docs-converter.py ../anki-manual manual en
python tools/docs-converter.py ../anki-faqs faqs ar
```
---------
Co-authored-by: Abdo <abdo@abdnh.net>
Co-authored-by: Fernando Lins <1887601+fernandolins@users.noreply.github.com>
## Linked issue (required)
Closes#5417
### Checklist (minimum)
- [x] I ran `./ninja check` or an equivalent relevant check locally.
- [x] I added or updated tests when the change is non-trivial or
behavior changed.
## Scope
- [x] This PR is focused on one change (no unrelated edits).
## Linked issue
Closes#5538
## Summary
This updates to Qt 6.11.2 patch release for security/crash fixes.
## How to test
Run `just test` and `just test-e2e`.
## Linked issue
Closes#5521
## Summary / motivation
Adds an Anki-focused unit testing guide for maintainers, contributors,
and coding assistants.
The guide:
- Defines principles for focused, deterministic, and maintainable tests.
- Covers happy paths, error paths, boundaries, regressions, and
test-first development.
- Provides guidance for Python, Python/Qt, Rust, and
Svelte/TypeScript/JavaScript.
- Clarifies the appropriate scope for UI tests and discourages testing
styling or volatile presentation details.
- Links the guide from the shared `CLAUDE.md`/`AGENTS.md` instructions.
## Linked issue (required)
Fixes#5516
## Summary / motivation (required)
Make the existing FSRS parameter unlock timing API available before the
parameter editor is mounted, and retain the host's chosen timeout when
FSRS is disabled and enabled again. Register the API and store its
timeout in the component's module script; keep click tracking local to
each editor instance.
## Steps to reproduce (required, use N/A if not applicable)
1. Open the deck-options page with FSRS disabled.
2. In that page's JavaScript console, run
`anki.setParameterUnlockClickTimeoutMs(1000)`.
3. Before this fix, the call throws `TypeError:
anki.setParameterUnlockClickTimeoutMs is not a function`.
## How to test (required)
### Checklist (minimum)
- [x] I ran `just check` locally.
- [x] I added a browser regression for the changed behavior.
### Details
- The regression failed on the original implementation at the pre-mount
setter call and passes with the fix.
- It checks configuration before the first mount, retention across
remounts, updates while absent and present, the three-click gate, the
default timeout, and reset after page reload. Playwright's clock
controls the click intervals.
- `just test-ts`: 61 tests passed.
- `just test-e2e`: 27 tests passed, using the repository's disposable
Anki harness.
- `just check`: passed, including 572 Rust tests and the Python,
formatting, lint, and type checks.
- The Qt checks emitted an audio-thread warning (`mw` was `None` in
`qt/aqt/sound.py`); all 93 Qt tests passed and `just check` exited
successfully.
## Before / after behavior (optional)
Before: the API exists only after the editor mounts, and mounting a new
editor resets the timeout to 500 ms.
After: the API exists when the deck-options JavaScript loads, and the
selected timeout lasts for the page's lifetime.
## Risk / compatibility / migration (optional)
Existing API names, the 500 ms default, and the three-click requirement
are preserved. Click counts and pending timers remain local to each
editor. No stored data or scheduling behavior changes.
## UI evidence (required for visual changes; otherwise N/A)
N/A.
## Scope
- [x] This PR is focused on one change.
Developed with assistance from Codex.
## Linked issue
Closes#5383
## Summary / motivation
`rslib/src/decks/service.rs`: the Protobuf entry point for all deck
operations, it had 0% test coverage. This PR adds a golden
`#[cfg(test)]` suite that exercises the happy path, error paths,
boundary cases, and key invariants of the `DecksService` layer and its
proto↔domain conversions.
No production code was changed: the module is a thin routing/conversion
layer with no logic that warranted refactoring. Testing at this boundary
is additive rather than duplicative: `reparent_decks`,
`remove_decks_and_child_decks` and the proto↔domain conversions had no
direct unit tests before.
Coverage of `decks/service.rs` goes from **0% → ~99.7% of lines**
## How to test
### Details
- `just check` passes (fmt, clippy, rust_test, and the rest of the
suite).
- Run just this module: `cargo test -p anki decks::service::tests`.
- Coverage inspected with `just test-rust --coverage --html`
- `decks/service.rs` reports ~99.7% line coverage.
Scenarios covered:
- proto↔domain conversions: full-field `Deck` round trip for both kinds,
`missing kind → InvalidInput`, `FilteredDeckForUpdate` field
preservation and `config: None → default`.
- create/retrieve, update (common + kind-specific persistence), rename
with descendant cascade, delete + card removal, reparent (incl.
`new_parent == 0`).
- name/tree listing: `include_filtered`, empty vs non-empty default
skipping, child-name filtering, `deck_tree` counts by `now`, legacy tree
JSON.
- current deck, collapsed-scope isolation, legacy JSON paths (incl.
`preserve_usn_and_mtime`), and filtered-deck build.
- Error paths assert the specific `AnkiError` variant (`NotFound` /
`InvalidInput`), not just `is_err()`.
## Linked issue
Closes#5386
## Summary
This increases test coverage of `rslib/src/notes/service.rs` and some
related modules (`rslib/src/adding.rs` and `rslib/src/notes/mod.rs`).
## How to test
Run `just test-rust --coverage --html` and view coverage reports.
## Linked issue
Closes#5541
## Summary
Add compatibility wrappers for AnkiPackageImporter/AnkiPackageExporter
to redirect the calls to the new APIs. This is intended as a temporary
compatibility workaround for the
[AnkiConnect](https://ankiweb.net/shared/info/2055492159) add-on, not as
a general wrapper for all removed APIs.
## Steps to reproduce (before)
Install AnkiConnect and confirm it fails at startup due to the removed
`anki.importing` and `anki.exporting` modules.
## How to test (after)
Export a sample apkg and run this script in the same directory to test
AnkiConnect's importPackage/exportPackage APIs.
```python
import json
import urllib.request
from pathlib import Path
def request(action, **params):
return {'action': action, 'params': params, 'version': 6}
def invoke(action, **params):
payload = json.dumps(request(action, **params)).encode('utf-8')
response = json.load(urllib.request.urlopen(urllib.request.Request('http://127.0.0.1:8765', payload)))
if len(response) != 2:
raise Exception('response has an unexpected number of fields')
if 'error' not in response:
raise Exception('response is missing required error field')
if 'result' not in response:
raise Exception('response is missing required result field')
if response['error'] is not None:
raise Exception(response['error'])
return response['result']
result = invoke('importPackage', path=str(Path(__file__).parent / 'sample.apkg'))
assert result is True
decks = invoke('deckNamesAndIds')
deck_to_export = next(name for name in decks if name != 'Default')
result = invoke('exportPackage', deck=deck_to_export, path=str(Path(__file__).parent / 'exported.apkg'), includeSched=True)
assert result is True
```