## Linked issue (required)
Fixes#4345.
## Summary / motivation (required)
`Notetype::merge_fields` and `merge_templates`
(`rslib/src/notetype/merge.rs`) used the **incoming** notetype's index
directly as a position into **our** own `fields`/`templates`. When the
incoming notetype has more fields than ours — e.g. a corrupt notetype
whose field matches one of ours a second time at an index past the end
of our list — `Vec::swap`/`Vec::insert` received an out-of-bounds index
and panicked:
```
thread '<unnamed>' panicked at rslib/src/notetype/merge.rs:42:33:
index out of bounds: the len is 22 but the index is 22
```
This made the affected deck impossible to import.
The fix clamps the target index to the valid range so the merge degrades
gracefully instead of panicking. All incoming fields/templates are still
merged in; a duplicate match simply collapses onto the existing entry
rather than crashing.
## Steps to reproduce (required, use N/A if not applicable)
1. Import a deck whose notetype has more fields than the local notetype
it maps to, where one incoming field matches an existing field a second
time (reported in the wild on corrupt decks —
https://forums.ankiweb.net/t/im-unable-to-import-any-decks/66497).
2. The import panics with `index out of bounds` in `notetype/merge.rs`.
## How to test (required)
### Checklist (minimum)
- [ ] 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 two regression tests — `merge_longer_other_with_duplicate_field`
and `merge_longer_other_with_duplicate_template`. Verified they **panic
on the unfixed code** and **pass with the fix**, and that the whole
`notetype::merge` module passes:
```
cargo test -p anki --lib notetype::merge::
test result: ok. 8 passed; 0 failed
```
`rustfmt --check` is clean on the changed file.
## Before / after behavior (optional)
Before: importing such a notetype panics and aborts the import. After:
the merge completes, mapping the duplicate onto the existing
field/template.
## Risk / compatibility / migration (optional)
Low — the clamp only changes behavior in the previously-panicking
out-of-bounds case; in-range merges are unaffected.
## UI evidence (required for visual changes; otherwise N/A)
N/A
## Scope
- [x] This PR is focused on one change (no unrelated edits).
Closes#4524.
Strip `fsrs_params_4/5/6` from each deck config when exporting with
`with_scheduling=false`. These weights are persistent on the exporter's
review history and shouldn't be exported.
Tested with new gather-layer unit tests and a round-trip apkg
export-and-reimport test; all 65 existing `import_export` tests pass.
---------
Signed-off-by: Christos Longros <chris.longros@gmail.com>
<!--
Title (for the Pull Request title field at the top):
Use a short prefix so the change type is obvious. You do not need to
repeat it in the body below.
Examples:
- fix: — bugfix
- feat: — feature
- refactor: — internal change without user-facing feature
- docs: — documentation only
- chore: — tooling, CI, deps, build housekeeping
- test: — tests only
-->
## Linked issue (required)
- Fixes#5117
## Summary / motivation (required)
`NetworkError::message` always appended the `network-details` line, so
an error with an empty `info` string rendered a dangling "Error
details:" label with nothing after it. Only append the details when
there is something to show.
The message is built in the shared backend, so the empty label showed up
on every client (it was reported on AnkiDroid) is still used unchanged
when details are present.
## Steps to reproduce (required, use N/A if not applicable)
Any network error
## How to test (required)
Added tests
### Checklist (minimum)
- [ ] 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
<!-- Commands, manual steps, edge cases, and what you observed -->
## Before / after behavior (optional)
<!-- For bugfixes: behavior before vs after. For other types: N/A or a
short note. -->
## Risk / compatibility / migration (optional)
<!-- Breaking changes, rollout notes, or N/A for small / low-risk PRs
-->
## UI evidence (required for visual changes; otherwise N/A)
<!-- Screenshot or short video -->
## Scope
- [x] This PR is focused on one change (no unrelated edits).
## Linked issue
Fixes#5109
## Summary / motivation
This changes the `sync_status()` method so that it reports `NoChanges`
for new empty collections. Currently it reports `FullSync` on a fresh
collection and this status persists even after syncs because the process
works as follows:
1. Anki checks the local collection's modification and schema change
timestamps against the time of the last sync (See
`sync_status_offline()`).
2. If the collection was never synced (`last_sync=0`)
`sync_status_offline()` returns `FullSync` as there's no special
handling for this case.
3. `sync_status_inner()` returns the value returned by
`sync_status_offline()` if it's not `NoChanges`, skipping the AnkiWeb
network request, so the remote collection's modification time is never
compared to the local modification time (they are both 0 on a fresh
collection & account, so `online_sync_status_check()` would have
returned `NoChanges` if not for the early return).
4. When the user clicks the sync button, the action required is
determined solely by `online_sync_status_check()`, which returns
`NoChanges` as noted earlier, so no sync is done and `last_sync` never
gets updated, causing the offline sync status to get stuck on
`FullSync`.
## Steps to reproduce (before)
1. Create a new AnkiWeb account and log in to it in a fresh Anki
profile. Notice the sync button is now red.
2. Click the sync button (without creating notes or making any DB
modifications to the collection) and confirm no prompt to accept the
"fake" full sync.
3. Notice the button is still red, indicating the full sync status
persists and won't be cleared until you make changes to the either the
local or remote collection.
## How to test (after)
Repeat the same process and confirm the sync button is never red for a
fresh collection.
## Linked issue
Closes#5126
## Summary
This prevents translation strings from the
https://github.com/ankitects/anki-desktop-ftl to be used in Rust/TS code
as AnkiMobile does not pull this module.
## Steps to reproduce (before)
Qt strings such as `addons-no-updates-available` were allowed to be used
in Rust: #5122
## How to test (after)
Revert #5122 and confirm you get a compile error with this PR.
## Linked issue (required)
Fixes#4750
## Summary / motivation (required)
`new_cards_required_normal()` builds a set of non-empty fields from the
note's actual fields, but only special-cases `Tags`. Other special
fields like `Deck`, `Subdeck`, `Card`, `CardFlag`, `Type`, and `CardID`
are always populated at render time but were missing from this set. This
caused `{{^Deck}}` conditionals to be incorrectly evaluated as
potentially rendering, making cards appear non-empty to the checker. The
fix adds all always-present special fields to `nonempty_fields`.
## Steps to reproduce (required)
1. Create a Basic (and reversed card) note with content in both fields
2. Change Card 1's front template to `{{^Deck}}{{Back}}{{/Deck}}`
3. Observe the previewer shows a blank card
4. Run Tools → Check → Empty Cards — no empty cards are reported
## How to test (required)
Run `./ninja check:pytest` — a new regression test
`test_empty_cards_special_fields` in `pylib/tests/test_cards.py` covers
this case.
### 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
New test verifies that a card with `{{^Deck}}{{Back}}{{/Deck}}` as its
front template is correctly detected as empty.
## Before / after behavior (optional)
Before: cards using special field conditionals like `{{^Deck}}` were not
detected as empty. After: they are correctly flagged.
## Risk / compatibility / migration (optional)
Low risk. Only affects empty card detection logic, not card rendering or
scheduling.
## UI evidence (required for visual changes; otherwise N/A)
N/A
## Scope
- [x] This PR is focused on one change (no unrelated edits).
Publish the media sync abort handle before spawning the worker, clear it
with a scope guard, and return `CollectionNotOpen` instead of panicking
if the collection is gone before the worker starts. This avoids a race
where closing the collection during media sync startup could panic.
<!--
Title (for the Pull Request title field at the top):
Use a short prefix so the change type is obvious. You do not need to
repeat it in the body below.
Examples:
- fix: — bugfix
- feat: — feature
- refactor: — internal change without user-facing feature
- docs: — documentation only
- chore: — tooling, CI, deps, build housekeeping
- test: — tests only
-->
## Linked issue (required)
fixes#4791
<!-- Fixes#123 / Closes#123 / Refs #123 -->
## Summary / motivation (required)
<!-- What this PR does and why. For larger changes, add enough context
for reviewers. -->
Previously, using set due date on a new card was broken for FSRS because
new cards didn't have a memory state which was how the set due date
function determined if FSRS is enabled or not. See
https://github.com/ankitects/anki/pull/4035#discussion_r2107488134.
This PR also makes it so that new cards behave as new cards after set
due date is used on them (Only while FSRS is enabled).
## Steps to reproduce (required, use N/A if not applicable)
<!-- Steps to reproduce: how to trigger the bug in the broken state (the
"before").
- Mainly for bugfixes;
- For bugs: numbered steps before the fix. For non-bugs: write N/A.
- use N/A for features, refactors, docs, chore, etc.
-->
1. create a new card
2. set it's due date
3. set it's due date again
4. the due date won't change
5. use grade now
6. the card will be scheduled based on the future date due date rather
than now
## How to test (required)
<!--- How to test: how you verified the change (checks, unit tests,
manual steps, edge cases — the "after" or general validation). --->
I followed the reproduction steps and instead, after using set due date
the card remains in a new state, and the saved interval is the inputted
number (with a minimum of one to prevent it appearing as a reset)
### Checklist (minimum)
- [X] I ran `./ninja check` or an equivalent relevant check locally.
- [ ] I added or updated tests when the change is non-trivial or
behavior changed.
### Details
<!-- Commands, manual steps, edge cases, and what you observed -->
## Before / after behavior (optional)
<!-- For bugfixes: behavior before vs after. For other types: N/A or a
short note. -->
## Risk / compatibility / migration (optional)
<!-- Breaking changes, rollout notes, or N/A for small / low-risk PRs
-->
If there are any ways I have not foreseen that an interval can be set to
0, then it may cause problems with this approach.
## UI evidence (required for visual changes; otherwise N/A)
<!-- Screenshot or short video -->
## Scope
- [X] This PR is focused on one change (no unrelated edits).
<!--
Title (for the Pull Request title field at the top):
Use a short prefix so the change type is obvious. You do not need to
repeat it in the body below.
Examples:
- fix: — bugfix
- feat: — feature
- refactor: — internal change without user-facing feature
- docs: — documentation only
- chore: — tooling, CI, deps, build housekeeping
- test: — tests only
-->
## Linked issue (required)
<!-- Fixes#123 / Closes#123 / Refs #123 -->
Refs #4080
## Summary / motivation (required)
<!-- What this PR does and why. For larger changes, add enough context
for reviewers. -->
bump fsrs version
## Steps to reproduce (required, use N/A if not applicable)
<!-- Steps to reproduce: how to trigger the bug in the broken state (the
"before").
- Mainly for bugfixes;
- For bugs: numbered steps before the fix. For non-bugs: write N/A.
- use N/A for features, refactors, docs, chore, etc.
-->
N/A
## How to test (required)
<!--- How to test: how you verified the change (checks, unit tests,
manual steps, edge cases — the "after" or general validation). --->
### 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
<!-- Commands, manual steps, edge cases, and what you observed -->
## Before / after behavior (optional)
<!-- For bugfixes: behavior before vs after. For other types: N/A or a
short note. -->
## Risk / compatibility / migration (optional)
<!-- Breaking changes, rollout notes, or N/A for small / low-risk PRs
-->
## UI evidence (required for visual changes; otherwise N/A)
<!-- Screenshot or short video -->
## Scope
- [x] This PR is focused on one change (no unrelated edits).
<!--
Title (for the Pull Request title field at the top):
Use a short prefix so the change type is obvious. You do not need to
repeat it in the body below.
Examples:
- fix: — bugfix
- feat: — feature
- refactor: — internal change without user-facing feature
- docs: — documentation only
- chore: — tooling, CI, deps, build housekeeping
- test: — tests only
-->
## Linked issue (required)
Fixes#4394
## Summary / motivation (required)
Currently, `card_state_updater` calls `get_config_bool` for
`BoolKey::Fsrs` and `BoolKey::FsrsShortTermWithStepsEnabled` on every
card answer, incurring a SQLite read each time.
With this PR, the flags are read once at queue-build time and then
threaded into `CardQueues` so that `card_state_updater` can read the
cached value instead of hitting the config DB on every answer.
When `card_queues` is `None` (unit tests, direct API calls outside the
reviewer), fall back to `get_config_bool` to preserve existing behavior.
This is the same pattern used for `load_balancer` in #3860.
## Steps to reproduce (required, use N/A if not applicable)
N/A — performance optimization, no user-facing change.
## How to test (required)
Not sure
### Checklist (minimum)
- [ ] I ran `./ninja check` or an equivalent relevant check locally.
- [ ] I added or updated tests when the change is non-trivial or
behavior changed.
### Details
<!-- Commands, manual steps, edge cases, and what you observed -->
## Before / after behavior (optional)
<!-- For bugfixes: behavior before vs after. For other types: N/A or a
short note. -->
## Risk / compatibility / migration (optional)
<!-- Breaking changes, rollout notes, or N/A for small / low-risk PRs
-->
## UI evidence (required for visual changes; otherwise N/A)
<!-- Screenshot or short video -->
## Scope
- [x] This PR is focused on one change (no unrelated edits).
## Linked issue (required)
Fixes#4431
## Summary / motivation (required)
Fix an FSRS edge case where fuzz can make a `Good` interval go
backwards, such as `4 -> 3`.
The existing safeguard only handled cases where the newly rounded
interval was already greater than the previous one. This change
preserves the previous interval when it still falls inside the current
fuzz range, and adds a regression test for the reported case.
## Steps to reproduce (required, use N/A if not applicable)
1. Start from an FSRS review state where the previous `Good` interval
was fuzzed up from 3 days to 4 days.
2. Review the card again in a state where FSRS computes a new `Good`
interval that rounds to 3 days but still has a fuzz range that includes
4.
3. Observe that the old logic schedules the next `Good` interval as 3
days.
## 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
- Ran `cargo test -p anki scheduler::states::review::test --
--nocapture`
- Added a regression test for the reported `4 -> 3` case
- Ran `./ninja check` successfully
## Before / after behavior (optional)
Before: a card whose previous interval had been fuzzed up to 4 days
could later show a `Good` interval of 3 days.
After: when the previous interval still lies inside the current fuzz
range, the interval is preserved instead of moving backwards.
## Risk / compatibility / migration (optional)
Low risk, as the change is limited to FSRS passing review interval
handling and no migration steps are required.
## 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 https://github.com/ankitects/anki/issues/5046
Fixes https://github.com/ankitects/anki/issues/4215
Related:
https://forums.ankiweb.net/t/search-search-problem-in-browse-windows/38603
## Summary / motivation (required)
When the browser search query contains non-breaking spaces (for e.g.,
when text is copied and pasted from a webpage or a card rendered in the
reviewer webview), the parser embeds the \u{a0} characters inside the
SQL query rather than treating them as word separators. SQL then tries
to search them as literal substrings, which never matches the note's
stored field text (which uses normal spaces), resulting in zero results
even though matching notes exist.
The fix normalizes \u{a0} to a regular space in parse(), before any
parsing begins, so it is consistently treated as a word separator
everywhere.
## Steps to reproduce (required, use N/A if not applicable)
1. Create a note with the text "hello world".
2. Copy text having a non-breaking space between "hello world" into the
browser's search bar (I am unable to paste a sample here) and press
Enter.
3. See that no results are returned.
## How to test (required)
In this branch, the same steps return the note as expected.
Checking the SQL query:
Apply this patch, set environment variable `set RUST_LOG=anki=debug`
before running Anki and then repeat the repro steps:
```diff
diff --git a/rslib/src/search/mod.rs b/rslib/src/search/mod.rs
index 0dd52dbc3..4fdab340c 100644
--- a/rslib/src/search/mod.rs
+++ b/rslib/src/search/mod.rs
@@ -24,6 +24,7 @@ use rusqlite::params_from_iter;
use rusqlite::types::FromSql;
use sqlwriter::RequiredTable;
use sqlwriter::SqlWriter;
+use tracing::debug;
pub use writer::replace_search_node;
use crate::browser_table::Column;
@@ -189,6 +190,7 @@ impl Collection {
let (mut sql, args) = writer.build_query(&top_node, mode.required_table())?;
self.add_order(&mut sql, item_type, mode)?;
+ debug!(sql = %sql, args = ?args);
let mut stmt = self.storage.db.prepare(&sql)?;
let ids: Vec<_> = stmt
```
On the main branch, the terminal prints:
```
DEBUG sql=select c.id from cards c, notes n where c.nid=n.id and ((n.sfld like ?1 escape '\' or n.flds like ?1 escape '\')) order by c.mod desc args=["%hello\u{a0}world%"]
```
On this branch, the terminal prints:
```
DEBUG sql=select c.id from cards c, notes n where c.nid=n.id and ((n.sfld like ?1 escape '\' or n.flds like ?1 escape '\') and (n.sfld like ?2 escape '\' or n.flds like ?2 escape '\')) order by c.mod desc args=["%hello%", "%world%"]
```
### Checklist (minimum)
- [ ] 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
<!-- Commands, manual steps, edge cases, and what you observed -->
## Before / after behavior (optional)
<!-- For bugfixes: behavior before vs after. For other types: N/A or a
short note. -->
## Risk / compatibility / migration (optional)
<!-- Breaking changes, rollout notes, or N/A for small / low-risk PRs
-->
## UI evidence (required for visual changes; otherwise N/A)
<!-- Screenshot or short video -->
## Scope
- [x] This PR is focused on one change (no unrelated edits).
---------
Co-authored-by: Abdo <abdo@abdnh.net>
## Linked issue (required)
Fixes#5025
## Summary / motivation (required)
`scheduler::answering::test::new_limited_by_reviews` in
`rslib/src/scheduler/answering/mod.rs` is time-of-day dependent and
fails when
run between roughly 3:50–4:00 GMT. The source carried a
`// FIXME: This fails between 3:50-4:00 GMT` comment documenting the
flakiness.
The test's final `answer_good()` schedules a learning card a 10-minute
step
ahead. Run shortly before the default 4am rollover, that step crosses
into the
next day, so the card is no longer counted as intraday learning and the
final
`assert_counts!(col, 0, 0, 0)` breaks.
This guards the test with the same `if col.timing_today()?.near_cutoff()
{ return Ok(()); }`
early-return that the sibling timing-sensitive tests in this file
(`state_application`, `elapsed_secs`) already use, and replaces the
FIXME with a
comment explaining the cause. `near_cutoff()`'s 25-minute margin safely
covers
the 10-minute step window.
## Steps to reproduce (required, use N/A if not applicable)
1. Set the system clock to a few minutes before the collection's daily
cutoff
(the default 4am rollover — i.e. ~03:50–03:59 in the collection's
timezone).
2. Run `cargo test -p anki --lib
scheduler::answering::test::new_limited_by_reviews`.
3. The final `assert_counts!(col, 0, 0, 0)` fails because the last
learning
card's 10-minute step lands after the cutoff.
## How to test (required)
### Checklist (minimum)
- [ ] 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
`cargo test -p anki --lib
scheduler::answering::test::new_limited_by_reviews`
passes, and the full `scheduler::answering` module passes (5/5). The fix
mirrors
the existing `near_cutoff()` guard already used by the two sibling
timing-sensitive tests in the same file.
## Before / after behavior (optional)
Before: the test fails non-deterministically in the ~10-minute window
before the
daily cutoff. After: it early-returns in the near-cutoff window
(25-minute
margin) and runs normally the rest of the day.
## Risk / compatibility / migration (optional)
N/A — test-only change, no production behavior affected.
## 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: Fernando Lins <1887601+fernandolins@users.noreply.github.com>
## Linked issue
Fixes#4924
## Summary/motivation
The scheduler/cards service layer that the front-end and gRPC clients
call into was largely untested.
This PR adds unit tests that exercise each method through the
`SchedulerService` / `CardsService` traits (proto-in / proto-out),
mirroring how users actually use them, and asserts the resulting state
in `Collection` storage.
**Out of scope**: pure FSRS math methods are intentionally left out.
## How to test
### Details
- `cargo test -p anki --lib scheduler::service::tests` — 42 passing.
- `cargo test -p anki --lib card::service::tests` — 1 passing.
- `cargo fmt --check` and lints clean.
- Tests are deterministic and isolated (each builds its own in-memory
`Collection`); the randomize test uses a retry loop with a permutation
invariant instead of a probabilistic assertion.
## Linked issue
Closes#4926
## Summary / motivation
`rslib/src/search/service/` had no test coverage. This PR adds tests
across two files (`mod.rs` and `search_node.rs`), covering:
- `TryFrom<SearchNode>` proto → domain conversions for all filter
variants
- `From<Rating>` and `From<CardState>` conversions
- `SearchService` RPC methods: `build_search_string`, `search_cards`,
`browser_row_for_id`, `replace_search_node`
- `SortMode` construction from proto, including the `Custom` fallback
- Error path: `build_search_string` with an empty group returns
`InvalidInput`
- Exact-match integration tests for deck, tag, and note-type filters in
`search_cards`
## How to test
```bash
just test-rust
```
## Linked issue
Closes#4925
## Summary / motivation
Adds a comprehensive unit test suite for
`rslib/src/notetype/service.rs`, which previously had zero tests. The
file implements the `NotetypesService` gRPC trait for `Collection` — 20
service methods and 6 `From` trait conversions used by both the Qt
frontend (via the legacy JSON adapter path) and the modern proto path.
Tests are organized into five commits by concern:
- **From trait conversions** (`NotetypeId` round-trip,
`ChangeNotetypeRequest ↔ ChangeNotetypeInput` mappings including the `-1
→ None` sentinel and `is_cloze` flag)
- **Legacy JSON adapters** (`add_notetype_legacy`,
`update_notetype_legacy`, `get_notetype_legacy`,
`get_stock_notetype_legacy` for all 6 stock kinds, and all 3 branches of
`add_or_update_notetype`)
- **CRUD service methods** (add, get, update, remove — happy paths,
error paths, and side effects such as card generation when a template is
added)
- **Auxiliary methods** (`get_notetype_names_and_counts`,
`get_aux_notetype_config_key`, `get_aux_template_config_key` including
the Cloze branch and the error path for unknown IDs)
- **Coverage threshold bump** (60 → 64)
## How to test
### Details
```bash
cargo test -p anki --lib "notetype::service::tests"
```
---------
Co-authored-by: Abdo <abdo@abdnh.net>
## Linked issue
Closes#4556Closes#4557Closes#4144Closes#4151Closes#4152Closes#4153Closes#4229Closes#4356Closes#4401Closes#4403Closes#4519Closes#4523Closes#4390Closes#4414Closes#4484
## Summary / motivation
After 4 months of exploring Briefcase for packaging, we're confident it
solves most problems with the uv launcher with less complexity and at a
lower maintenance cost, especially with the parallel work on the release
CI, which we already used to make 5 releases.
This also removes platform-specific packaging/signing code used to
produce macOS dmg files and Windows NSIS installers, which is now
largely handled by Briefcase and the release CI.
The custom install scripts for Linux are mostly preserved under
qt/installer/linux-template and used in the Briefcase build.
## How to test
- [ ] No build problems.
- [ ] The `tools/build-installer` scripts still work.
- [ ] No CI/release workflow issues.
- [ ] No user-visible changes in dev environment and Briefcase build.
## Linked issue
Fixes CI failure introduced by #4927
## Summary / motivation
The column-position tests added in the previous PR were failing in CI on
Linux. Anki GUIDs use a base91 charset that includes `"` (double quote).
When a GUID contains `"`, the CSV writer correctly wraps the field in
quotes, but the tests were splitting the raw line by `\t`, which
returned the surrounding quotes as part of the value:
```
left: ""AtI#}YcFn"" ← field value including CSV quotes right: "AtI#}YcFn" ← the actual GUID
```
The fix replaces manual `split('\t')` with `csv::ReaderBuilder` in both
affected tests. This is also the semantically correct approach: the same
parser the importer uses, which automatically unquotes fields.
The bug only manifested on Linux because the randomly generated GUID on
macOS happened not to contain `"`.
## How to test
```bash
cargo test -p anki --lib "import_export::text::csv::export::tests"
```
All 18 tests pass.
## Linked issue
Closes#4927
## Summary / motivation
`rslib/src/import_export/service.rs` and
`rslib/src/import_export/text/csv/export.rs` had no unit test coverage.
This PR adds tests for the meaningful logic in both modules: type
conversions, error propagation, config defaults, and CSV
structure/content.
**`service.rs`**
- All 4 branches of `From<ExportLimit> for SearchNode` (None, DeckId,
NoteIds, CardIds)
- `get_import_anki_package_presets` default config values
- `export_card_csv` / `export_note_csv` `UInt32` return type
- Error propagation for all I/O functions (`import_anki_package`,
`import_csv`, `import_json_file`, `import_json_string`,
`export_card_csv`, `export_note_csv`, `export_anki_package`)
- Success paths using a real `.apkg` fixture, temp CSV, and temp JSON
files
- `From<OpOutput<NoteLog>> for ImportResponse` field mapping
**`text/csv/export.rs`**
- Card CSV: header format, empty collection writes header, count
(single/multiple/filtered by deck), front/back in correct tab-separated
columns, HTML preserved and stripped
- Note CSV: count (single/multiple/filtered by deck), exact column index
values, column headers absent when metadata disabled, fields and GUID in
correct columns, GUID omitted when `with_guid: false`, HTML preserved
and stripped
**`text/csv/metadata.rs`**
- Extends `should_detect_valid_delimiters` to cover the last-resort
`Delimiter::Space` fallback when the file has no recognizable separator
in its content
## How to test
```bash
cargo test -p anki --lib "import_export::service::tests"
cargo test -p anki --lib "import_export::text::csv::export::tests"
cargo test -p anki --lib "import_export::text::csv::metadata::test::should_detect_valid_delimiters"
```
All 18 tests in export.rs, all tests in service.rs, and the delimiter
test pass.
## Linked issue
Closes#4928
## Summary / motivation
`learning.rs` and `relearning.rs` had 0% test coverage. They implement
the core scheduling logic for new and lapsed cards — a regression here
affects every user's review queue silently. This PR adds unit tests to
establish a safety net before future migrations or FSRS changes touch
this code.
## How to test
```bash
cargo test -p anki --lib "states::learning::tests"
cargo test -p anki --lib "states::relearning::tests"
```
## Details
`learning.rs` — tests covering all four buttons under SM-2: again (reset
to first step, no-steps graduation), hard (same step, no-steps
graduation), good (advance or graduate), easy (always graduates,
interval ordering invariant), plus FSRS routing stubs.
`relearning.rs` — tests covering SM-2 transitions (lapse penalty, step
keep/reset/graduate), FSRS routing (long interval → Review, short-term →
Relearning for all buttons), and multi-step behavior (again resets, hard
keeps, good advances through intermediate steps).
Strip HTML from note type and template names in the Empty Cards screen.
## How to test
```
cargo test -p anki --lib notetype::emptycards
```
---------
Co-authored-by: Fernando Lins <1887601+fernandolins@users.noreply.github.com>
<!--
Title (for the Pull Request title field at the top):
Use a short prefix so the change type is obvious. You do not need to
repeat it in the body below.
Examples:
- fix: — bugfix
- feat: — feature
- refactor: — internal change without user-facing feature
- docs: — documentation only
- chore: — tooling, CI, deps, build housekeeping
- test: — tests only
-->
## Linked issue (required)
- #4433
- #4716
## Summary / motivation (required)
This PR modifies `Check Media` to dedupe filenames case-insensitively so
as to avoid files that were added on case-sensitive filesystems
potentially being overwritten on case-insensitive ones
## Steps to reproduce (required, use N/A if not applicable)
N/A
## How to test (required)
Given a.jpg (hash 1), A.JPG (hash 2) and a.JPG (hash 1) as existing
media files on a case-sensitive fs, see that `Check Media` renames A.JPG
to a-2.jpg and a.JPG to a.jpg
### Checklist (minimum)
- [x] I ran `./ninja check` or an equivalent relevant check locally.
- [ ] 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).
<!--
Title (for the Pull Request title field at the top):
Use a short prefix so the change type is obvious. You do not need to
repeat it in the body below.
Examples:
- fix: — bugfix
- feat: — feature
- refactor: — internal change without user-facing feature
- docs: — documentation only
- chore: — tooling, CI, deps, build housekeeping
- test: — tests only
-->
## Linked issue (required)
- #4716
## Summary / motivation (required)
Currently, on filesystems where the media folder is treated as
case-insensitive, `[sound:BLAH.mp3]` and `[sound:blah.mp3]` point to the
same file on disk (if any), but one of the two would be considered as a
ref pointing to a missing file by the media checker which assumes
case-sensitivity
## Steps to reproduce (required, use N/A if not applicable)
See linked pr
## How to test (required)
On windows/macos, add `blah.mp3`, rename `blah.mp3` to `Blah.mp3` in the
ref, run `Check Media` and see that `Blah.mp3` isn't considered missing
### Checklist (minimum)
- [x] I ran `./ninja check` or an equivalent relevant check locally.
- [ ] 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).
<!--
Title (for the Pull Request title field at the top):
Use a short prefix so the change type is obvious. You do not need to
repeat it in the body below.
Examples:
- fix: — bugfix
- feat: — feature
- refactor: — internal change without user-facing feature
- docs: — documentation only
- chore: — tooling, CI, deps, build housekeeping
- test: — tests only
-->
## Linked issue (required)
- #4851
## Summary / motivation (required)
I made a mistake in #4851 by not lowercasing in the case where a file
with the same name but different contents already exists, leading to
inconsistent behaviour across platforms
## Steps to reproduce (required, use N/A if not applicable)
See linked pr
## How to test (required)
Run ./check on linux and windows and see that the same lowercasing
behaviour occurs on both
### Checklist (minimum)
- [x] I ran `./ninja check` or an equivalent relevant check locally.
- [ ] 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).
<!--
Title (for the Pull Request title field at the top):
Use a short prefix so the change type is obvious. You do not need to
repeat it in the body below.
Examples:
- fix: — bugfix
- feat: — feature
- refactor: — internal change without user-facing feature
- docs: — documentation only
- chore: — tooling, CI, deps, build housekeeping
- test: — tests only
-->
## Linked issue (required)
Fixes#4716
## Summary / motivation (required)
#4435 made it so that all new added media would have lowercased names,
but this was bugged and led to media refs pointing to inexistent files
on case-insensitive filesystems
The fix proposed is to try adding new files with lowercased names only
if they don't already exist, using the existing file otherwise
## Steps to reproduce (required, use N/A if not applicable)
See linked issue
## How to test (required)
Try copy-pasting a media file from the Edit window of an existing note
to the Add window and see that the filename in the resulting media ref
isn't forced to be lowercased
When pasting a new file, see that the filename is now lowercased
### Checklist (minimum)
- [x] I ran `./ninja check` or an equivalent relevant check locally.
- [ ] 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 (required)
[[<!-- Fixes#123 / Closes#123 / Refs #123
-->](https://forums.ankiweb.net/t/anki-collection-collection-export-anki-package-crashes-if-given-a-relative-path-with-one-component/69562)](https://forums.ankiweb.net/t/anki-collection-collection-export-anki-package-crashes-if-given-a-relative-path-with-one-component/69562)
## Summary / motivation (required)
See discussion about this on [the
forum](https://forums.ankiweb.net/t/anki-collection-collection-export-anki-package-crashes-if-given-a-relative-path-with-one-component/69562)
## Steps to reproduce (required, use N/A if not applicable)
`demo.py`:
```python
import tempfile
from pathlib import Path
import anki.collection
with tempfile.TemporaryDirectory() as tempdir:
tempdir = Path(tempdir)
col = anki.collection.Collection(str(tempdir / "temp.anki2"))
col.export_anki_package(
out_path="output.apkg",
options=anki.collection.ExportAnkiPackageOptions(),
limit=None,
)
```
```console
$ python demo.py
Traceback (most recent call last):
[... some noise elided here ...]
File "/nix/store/1w7swjzmjp7171yspih1q07a9vgacjzb-dev-env/lib/python3.14/site-packages/anki/_backend_generated.py", line 2037, in export_anki_package
raw_bytes = self._run_command(37, 4, message.SerializeToString())
File "/nix/store/1w7swjzmjp7171yspih1q07a9vgacjzb-dev-env/lib/python3.14/site-packages/anki/_backend.py", line 171, in _run_command
raise backend_exception_to_pylib(err)
anki.errors.BackendIOError: Failed to open '': No such file or directory (os error 2)
```
## How to test (required)
I added a unit test. It passes.
### Checklist (minimum)
- [ ] 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
<!-- Commands, manual steps, edge cases, and what you observed -->
## Before / after behavior (optional)
<!-- For bugfixes: behavior before vs after. For other types: N/A or a
short note. -->
## Risk / compatibility / migration (optional)
<!-- Breaking changes, rollout notes, or N/A for small / low-risk PRs
-->
## UI evidence (required for visual changes; otherwise N/A)
<!-- Screenshot or short video -->
## Scope
- [x] This PR is focused on one change (no unrelated edits).
---------
Co-authored-by: Abdo <abdo@abdnh.net>
## Linked issue
#4678
## Summary
This enables native Windows ARM64 builds for Briefcase. Depends on #4797
## How to test
- Run `./tools/ninja installer` in a Windows ARM64 machine.
- Check the architecture of the installer under `./out/installer/dist`
by going to Properties > Compatibility and confirming emulation settings
are disabled.
- Install the package and confirm Anki.exe is a native binary.
- Open Anki, go to the [debug
console](https://docs.ankiweb.net/misc.html#debug-console) and run the
following code to check the architecture of the Python build:
```python
import platform
print(platform.machine(), platform.python_compiler())
```
migrates Anki Desktop packaging from the legacy
NSIS/uv-based installer to [BeeWare
Briefcase](https://briefcase.readthedocs.io/). This branch integrates
work from many related issues and PRs to deliver cross-platform native
installers (MSI on Windows, .app on macOS, PyInstaller on Linux) with
code signing, notarization, and file association support.
## Integrated PRs
- #4585 — Set up Briefcase
- #4596 — Add Briefcase icons
- #4598 — Handle Briefcase file associations
- #4601 — Add Briefcase app permissions
- #4609 — Customize Briefcase's MSI installer
- #4616 — Set up Briefcase code signing and notarization
- #4618 — Fix Briefcase packaging for x86 Macs
- #4623 — Customize Briefcase's Linux template
- #4627 — List required Debian packages for Briefcase installer
- #4630 — Update Briefcase's Windows template
- #4631 — Rewrite Linux install/uninstall scripts for PyInstaller
- #4638 — Use PyInstaller on Linux
- #4645 — Update installer docs
- #4654 — Disable Briefcase's universal builds for macOS
- #4672 — Deal with existing NSIS installations in MSI installer
- #4676 — Remove duplicate Briefcase icons
- #4677 — Tweak Linux scripts for new installer
- #4709 — Add anki-console.bat to Briefcase's Windows package
## Related Issues
- #4557 — Evaluate BeeWare Briefcase for Anki packaging and distribution
- #4678 — Support native Windows ARM64 builds for Briefcase
- #4688 — Linux installer: migrate to PyInstaller and rewrite install
scripts
- #4689 — Investigate startup performance with Briefcase
- #4690 — Specify required Linux system packages for Briefcase
- #4691 — Investigate Windows ARM64 support with Briefcase
- #4692 — Test on Linux ARM with Briefcase
- #4693 — Separate ARM and Intel macOS releases
- #4694 — Update developer documentation for Briefcase installer
- #4695 — Support upgrade/downgrade with the Briefcase installer
- #4696 — Update user documentation for new installer
- #4702 — Update Briefcase's Windows template with upstream security fix
and OS version check
- #4703 — Follow-up tweaks to Linux install/uninstall scripts
## Related PRs
- #4619 — Enable Windows ARM64 support
- #4632 — Release action
---------
Co-authored-by: Abdo <abdo@abdnh.net>
Co-authored-by: Andrew Sanchez <andrewsanchez@users.noreply.github.com>
Co-authored-by: Fernando Lins <1887601+fernandolins@users.noreply.github.com>
The Problem
The "learn now" add-on can bulk-add many new cards to the learn queue
with due == now. When a user answers some of those and fails them, the
failed cards are scheduled for a few minutes later, but the current
queue-building behavior requires the user to finish the remaining "learn
now" cards (due == now) before the failed cards are shown again. This
increases the chance of repeated failures and reduces learning
efficiency.
The Change
The queue builder now partitions intraday learning cards into two
groups: previously-attempted (reps > 0) and never-attempted (reps == 0).
Within each group cards remain ordered by due time. The builder
concatenates the groups with previously-attempted cards first.
Why this fixes the problem
Failed learning cards (which now have reps > 0 and a later due
timestamp) will be prioritized ahead of never-attempted cards added by
"learn now". This ensures the user sees the failed cards as soon as they
are due, reducing repeated failures.
Why changes to Anki rather than the add-on
As far as I know, there is nothing that the add-on can do to improve
this situation. Secondly, I think that Anki should, at some point,
natively include the "Learn now" feature as it is quite useful when the
user wants to introduce specific cards (for e.g., for an upcoming exam)
without creating a temporary deck for that purpose. "Learn now" is also
a good way to solve one aspect of the Set Due Date misuse problem (see
https://github.com/open-spaced-repetition/fsrs4anki/issues/675#issuecomment-2548839346).
Fixes#4581
`contains_cloze_replacement()` was doing a shallow check, missing cloze
fields nested inside conditionals. Delegating to
`all_referenced_cloze_field_names()` fixes the inconsistency with
`contains_field_replacement()`
the load balancer only looks at cards in the same preset when balancing
cards. which means that if a sibling is in a different preset, it
doesn't get avoided. this is not ideal. so heres a fix.
and while I was here I added some extra weighting so it tries to spread
siblings out a bit.
The AV_TAGS regex matches an old format of the `anki:tts` tag
(`[anki:tts][lang=en_US]` rather than `[anki:tts lang=en_US]`). It was
originally updated when the feature got introduced in
0942ffbff6
but then was never adjusted to reflect later changes to the syntax.
I removed it from the pattern rather than updating it to match, because
AV_TAGS is only used for matching filenames nowadays (stripping is
handled by a different module).
* Add regression test for #1909 (long filename rename not reported as unused)
* Add Brett Schwartz to CONTRIBUTORS
* Use MAX_MEDIA_FILENAME_LENGTH constant and 'a'
* Update to Rust 1.90
* Fix clippy errors
* Update to 1.92
* Update cargo-deny again
Deno crate was using a CVSS version 4.0, which wasn't supported
---------
Co-authored-by: Damien Elmes <gpg@ankiweb.net>