150150 SuggestionTypeSearchNode,
151151 UpdatedInTheLastXDaysSearchNode,
152152 _on_protect_fields_action,
153+ _on_reset_deck_action,
154+ _on_reset_local_changes_action,
153155 _on_reset_optional_tags_action,
154156)
155157from ankihub.gui.browser.custom_search_nodes import (
@@ -6727,6 +6729,7 @@ def test_reset_local_changes_to_notes(
67276729 mock_client_get_note_type: MockClientGetNoteType,
67286730 mocker: MockerFixture,
67296731):
6732+ """User-initiated Reset (strip=False): personal tags and globally protected fields survive."""
67306733 with anki_session_with_addon_data.profile_loaded():
67316734 mw = anki_session_with_addon_data.mw
67326735
@@ -6736,13 +6739,12 @@ def test_reset_local_changes_to_notes(
67366739 basic_note_1 = mw.col.get_note(NoteId(1608240029527))
67376740 basic_note_2 = mw.col.get_note(NoteId(1608240057545))
67386741
6739- # Edit Front + Back, tag both as personally-protected (mirrors the auto-protect
6740- # hook's behaviour on real edits), and move the note to a different deck. Back is
6741- # also marked as globally protected for this note type — its protect tag should
6742- # survive the reset (user-authored intent that should outlast global protection).
6742+ # Edit Front + Back and move the note. Front is personally protected (auto-protect).
6743+ # Back is only globally protected — no personal tag — so a surviving edit proves
6744+ # the importer honours protected_fields, not AnkiHub_Protect::Back.
67436745 basic_note_1["Front"] = "changed front"
67446746 basic_note_1["Back"] = "changed back"
6745- basic_note_1.tags = ["AnkiHub_Protect ::Front", "AnkiHub_Protect::Back "]
6747+ basic_note_1.tags = [f"{TAG_FOR_PROTECTING_FIELDS} ::Front"]
67466748 basic_note_1.flush()
67476749 mw.col.set_deck(basic_note_1.card_ids(), 1)
67486750
@@ -6754,19 +6756,17 @@ def test_reset_local_changes_to_notes(
67546756 mocker.patch.object(AnkiHubClient, "get_protected_tags")
67556757 mock_client_get_note_type([note_type for note_type in mw.col.models.all()])
67566758
6757- # reset local changes
67586759 nids = ankihub_db.anki_nids_for_ankihub_deck(ah_did)
6759- reset_local_changes_to_notes(nids=nids, ah_did=ah_did, strip_personal_protect_tags=True )
6760+ reset_local_changes_to_notes(nids=nids, ah_did=ah_did, strip_personal_protect_tags=False )
67606761
6761- # Front: not globally protected → personal-protect tag stripped, field reset.
6762- # Back: globally protected → field stays edited (importer respects protected_fields)
6763- # and the personal-protect tag survives.
6762+ # Front: personally protected → edit and protect tag survive (NRT-897).
6763+ # Back: globally protected, no personal tag → field stays edited.
67646764 # Note is still in the deck it was moved to (Reset shouldn't move cards between decks).
67656765 basic_note_1.load()
6766- assert basic_note_1["Front"] == "This is the front 1 "
6766+ assert basic_note_1["Front"] == "changed front"
67676767 assert basic_note_1["Back"] == "changed back"
6768- assert "AnkiHub_Protect ::Front" not in basic_note_1.tags
6769- assert "AnkiHub_Protect ::Back" in basic_note_1.tags
6768+ assert f"{TAG_FOR_PROTECTING_FIELDS} ::Front" in basic_note_1.tags
6769+ assert f"{TAG_FOR_PROTECTING_FIELDS} ::Back" not in basic_note_1.tags
67706770 assert basic_note_1.cards()
67716771 for card in basic_note_1.cards():
67726772 assert card.did == 1
@@ -6779,15 +6779,45 @@ def test_reset_local_changes_to_notes(
67796779 assert mw.col.decks.name(card.did) == "Testdeck"
67806780
67816781
6782- def test_reset_local_changes_to_notes_without_stripping_personal_protect_tags(
6782+ def test_reset_local_changes_to_notes_strips_personal_protect_tags(
6783+ anki_session_with_addon_data: AnkiSession,
6784+ install_sample_ah_deck: InstallSampleAHDeck,
6785+ mock_client_get_note_type: MockClientGetNoteType,
6786+ mocker: MockerFixture,
6787+ ):
6788+ """True still strips personal protect tags except those matching globally protected fields."""
6789+ with anki_session_with_addon_data.profile_loaded():
6790+ mw = anki_session_with_addon_data.mw
6791+
6792+ _, ah_did = install_sample_ah_deck()
6793+
6794+ basic_note_1 = mw.col.get_note(NoteId(1608240029527))
6795+ basic_note_1["Front"] = "changed front"
6796+ basic_note_1["Back"] = "changed back"
6797+ basic_note_1.tags = [f"{TAG_FOR_PROTECTING_FIELDS}::Front", f"{TAG_FOR_PROTECTING_FIELDS}::Back"]
6798+ basic_note_1.flush()
6799+
6800+ mocker.patch.object(AnkiHubClient, "get_protected_fields", return_value={basic_note_1.mid: ["Back"]})
6801+ mocker.patch.object(AnkiHubClient, "get_protected_tags")
6802+ mock_client_get_note_type([note_type for note_type in mw.col.models.all()])
6803+
6804+ reset_local_changes_to_notes(nids=[basic_note_1.id], ah_did=ah_did, strip_personal_protect_tags=True)
6805+
6806+ basic_note_1.load()
6807+ assert basic_note_1["Front"] == "This is the front 1"
6808+ assert basic_note_1["Back"] == "changed back"
6809+ assert f"{TAG_FOR_PROTECTING_FIELDS}::Front" not in basic_note_1.tags
6810+ assert f"{TAG_FOR_PROTECTING_FIELDS}::Back" in basic_note_1.tags
6811+
6812+
6813+ def test_reset_local_changes_to_notes_keeps_personally_protected_fields(
67836814 anki_session_with_addon_data: AnkiSession,
67846815 install_ah_deck: InstallAHDeck,
67856816 import_ah_note: ImportAHNote,
67866817 mock_client_get_note_type: MockClientGetNoteType,
67876818 mocker: MockerFixture,
67886819):
6789- """The database check resets decks to repair add-on data, not because the user asked to
6790- discard edits, so it must leave personally protected content alone."""
6820+ """Unprotected fields reset; personally protected fields are left alone even if not globally protected."""
67916821 with anki_session_with_addon_data.profile_loaded():
67926822 ah_did = install_ah_deck()
67936823 note_info = import_ah_note(ah_did=ah_did)
@@ -6804,14 +6834,72 @@ def test_reset_local_changes_to_notes_without_stripping_personal_protect_tags(
68046834
68056835 reset_local_changes_to_notes(nids=[note.id], ah_did=ah_did, strip_personal_protect_tags=False)
68066836
6807- # Back is personally protected: content and tag both survive, even though the field
6808- # is not globally protected. Front is unprotected and gets reset as usual.
68096837 note.load()
68106838 assert note["Back"] == "personal content"
68116839 assert f"{TAG_FOR_PROTECTING_FIELDS}::Back" in note.tags
68126840 assert note["Front"] == note_info.fields[0].value
68136841
68146842
6843+ def test_reset_local_changes_to_notes_keeps_fields_when_protect_all_tag(
6844+ anki_session_with_addon_data: AnkiSession,
6845+ install_ah_deck: InstallAHDeck,
6846+ import_ah_note: ImportAHNote,
6847+ mock_client_get_note_type: MockClientGetNoteType,
6848+ mocker: MockerFixture,
6849+ ):
6850+ """AnkiHub_Protect::All (Protect Fields → all) blocks Reset of every field when strip=False."""
6851+ with anki_session_with_addon_data.profile_loaded():
6852+ ah_did = install_ah_deck()
6853+ note_info = import_ah_note(ah_did=ah_did)
6854+
6855+ note = aqt.mw.col.get_note(ankihub_db.anki_nid_for_ankihub_nid(note_info.ah_nid))
6856+ note["Front"] = "changed front"
6857+ note["Back"] = "changed back"
6858+ note.tags = [TAG_FOR_PROTECTING_ALL_FIELDS]
6859+ aqt.mw.col.update_note(note)
6860+
6861+ mocker.patch.object(AnkiHubClient, "get_protected_fields", return_value={})
6862+ mocker.patch.object(AnkiHubClient, "get_protected_tags", return_value=[])
6863+ mock_client_get_note_type([note_type for note_type in aqt.mw.col.models.all()])
6864+
6865+ reset_local_changes_to_notes(nids=[note.id], ah_did=ah_did, strip_personal_protect_tags=False)
6866+
6867+ note.load()
6868+ assert note["Front"] == "changed front"
6869+ assert note["Back"] == "changed back"
6870+ assert TAG_FOR_PROTECTING_ALL_FIELDS in note.tags
6871+
6872+
6873+ def test_reset_local_changes_to_notes_strips_protect_all_tag(
6874+ anki_session_with_addon_data: AnkiSession,
6875+ install_ah_deck: InstallAHDeck,
6876+ import_ah_note: ImportAHNote,
6877+ mock_client_get_note_type: MockClientGetNoteType,
6878+ mocker: MockerFixture,
6879+ ):
6880+ """True strips AnkiHub_Protect::All; it is never treated as a globally protected field tag."""
6881+ with anki_session_with_addon_data.profile_loaded():
6882+ ah_did = install_ah_deck()
6883+ note_info = import_ah_note(ah_did=ah_did)
6884+
6885+ note = aqt.mw.col.get_note(ankihub_db.anki_nid_for_ankihub_nid(note_info.ah_nid))
6886+ note["Front"] = "changed front"
6887+ note["Back"] = "changed back"
6888+ note.tags = [TAG_FOR_PROTECTING_ALL_FIELDS]
6889+ aqt.mw.col.update_note(note)
6890+
6891+ mocker.patch.object(AnkiHubClient, "get_protected_fields", return_value={})
6892+ mocker.patch.object(AnkiHubClient, "get_protected_tags", return_value=[])
6893+ mock_client_get_note_type([note_type for note_type in aqt.mw.col.models.all()])
6894+
6895+ reset_local_changes_to_notes(nids=[note.id], ah_did=ah_did, strip_personal_protect_tags=True)
6896+
6897+ note.load()
6898+ assert note["Front"] == note_info.fields[0].value
6899+ assert note["Back"] == note_info.fields[1].value
6900+ assert TAG_FOR_PROTECTING_ALL_FIELDS not in note.tags
6901+
6902+
68156903def test_db_check_resets_decks_without_stripping_personal_protect_tags(
68166904 anki_session_with_addon_data: AnkiSession,
68176905 install_ah_deck: InstallAHDeck,
@@ -6829,6 +6917,39 @@ def test_db_check_resets_decks_without_stripping_personal_protect_tags(
68296917 assert reset_mock.call_args.kwargs["strip_personal_protect_tags"] is False
68306918
68316919
6920+ def _run_taskman_with_progress(*args, **kwargs):
6921+ task = kwargs.get("task") or args[0]
6922+ kwargs["on_done"](future_with_result(task()))
6923+
6924+
6925+ def test_browser_reset_does_not_strip_personal_protect_tags(
6926+ anki_session_with_addon_data: AnkiSession,
6927+ install_ah_deck: InstallAHDeck,
6928+ import_ah_note: ImportAHNote,
6929+ mocker: MockerFixture,
6930+ ):
6931+ """Pins NRT-897: both browser Reset entry points pass strip_personal_protect_tags=False."""
6932+ with anki_session_with_addon_data.profile_loaded():
6933+ ah_did = install_ah_deck()
6934+ note_info = import_ah_note(ah_did=ah_did)
6935+ nid = ankihub_db.anki_nid_for_ankihub_nid(note_info.ah_nid)
6936+
6937+ reset_mock = mocker.patch("ankihub.gui.browser.browser.reset_local_changes_to_notes")
6938+ mocker.patch.object(aqt.mw.taskman, "with_progress", side_effect=_run_taskman_with_progress)
6939+ mocker.patch("ankihub.gui.browser.browser.tooltip")
6940+ mocker.patch("ankihub.gui.browser.browser.choose_ankihub_deck", return_value=ah_did)
6941+ mocker.patch("ankihub.gui.browser.browser.ask_user", return_value=True)
6942+ mocker.patch.object(aqt.mw, "reset")
6943+
6944+ browser = mocker.Mock()
6945+ _on_reset_local_changes_action(browser, [nid])
6946+ assert reset_mock.call_args.kwargs["strip_personal_protect_tags"] is False
6947+
6948+ reset_mock.reset_mock()
6949+ _on_reset_deck_action(browser)
6950+ assert reset_mock.call_args.kwargs["strip_personal_protect_tags"] is False
6951+
6952+
68326953def test_migrate_profile_data_from_old_location(
68336954 anki_session_with_addon_before_profile_support: AnkiSession,
68346955 mocker: MockerFixture,
0 commit comments