Reintroduce a "modern" editor interface à la git-commit or git-rebase
Marco Ricci

Marco Ricci commited on 2025-02-06 14:27:21
Zeige 4 geänderte Dateien mit 278 Einfügungen und 56 Löschungen.


Reintroduce the previous git-like editor interface as the "modern"
editor interface to the "vault" subcommand, and provide explicit options
`--modern-editor-interface` and `--vault-legacy-editor-interface`,
defaulting to the latter for compatibility.

Furthermore, for the legacy interface, introduce a backup copy of the
notes to guard against accidental data loss, because the legacy
interface offers no way to abort mid-editing, making fatal mistakes all
the more likely.
... ...
@@ -168,6 +168,7 @@ config_filename_table = {
168 168
     # TODO(the-13th-letter): Remove the old settings.json file.
169 169
     # https://the13thletter.info/derivepassphrase/latest/upgrade-notes.html#v1.0-old-settings-file
170 170
     'old settings.json': 'settings.json',
171
+    'notes backup': 'old-notes.txt',
171 172
 }
172 173
 
173 174
 
... ...
@@ -809,7 +809,8 @@ class Label(enum.Enum):
809 809
     """"""
810 810
     DERIVEPASSPHRASE_VAULT_NOTES_INSTRUCTION_TEXT = commented(
811 811
         "This instruction text is shown above the user's old stored notes "
812
-        'for this service, if any.  '
812
+        'for this service, if any, if the recommended '
813
+        '"modern" editor interface is used.  '
813 814
         'The next line is the cut marking defined in '
814 815
         'Label.DERIVEPASSPHRASE_VAULT_NOTES_MARKER.'
815 816
     )(
... ...
@@ -826,6 +827,21 @@ class Label(enum.Enum):
826 827
 """,
827 828
     )
828 829
     """"""
830
+    DERIVEPASSPHRASE_VAULT_NOTES_LEGACY_INSTRUCTION_TEXT = commented(
831
+        "This instruction text is shown above the user's old stored notes "
832
+        'for this service, if any, if the vault(1)-compatible '
833
+        '"legacy" editor interface is used.  '
834
+        'The interface does not support commentary in the notes, '
835
+        'so we fill this with obvious placeholder text instead.  '
836
+        '(Please replace this with what *your* language/culture would '
837
+        'obviously recognize as placeholder text.)'
838
+    )(
839
+        'Label :: Help text :: Explanation',
840
+        'Lorem ipsum dolor sit amet, consectetur adipiscing elit, '
841
+        'sed do eiusmod tempor incididunt ut labore '
842
+        'et dolore magna aliqua.',
843
+    )
844
+    """"""
829 845
     DEPRECATED_COMMAND_LABEL = commented(
830 846
         'We use this format string to indicate, at the beginning '
831 847
         "of a command's help text, that this command is deprecated.",
... ...
@@ -1082,6 +1098,21 @@ class Label(enum.Enum):
1082 1098
         'when exporting, export as JSON (default) or POSIX sh',
1083 1099
     )
1084 1100
     """"""
1101
+    DERIVEPASSPHRASE_VAULT_EDITOR_INTERFACE_HELP_TEXT = commented(
1102
+        'The corresponding option is displayed as '
1103
+        '"--modern-editor-interface / --vault-legacy-editor-interface", '
1104
+        'so you may want to hint that the default (legacy) '
1105
+        'is the second of those options.  '
1106
+        'Though the vault(1) legacy editor interface clearly has deficiencies '
1107
+        'and (in my opinion) should only be used for compatibility purposes, '
1108
+        'the one-line help text should try not to sound too judgmental, '
1109
+        'if possible.',
1110
+    )(
1111
+        'Label :: Help text :: One-line description',
1112
+        'edit notes using the modern editor interface '
1113
+        'or the vault-like legacy one (default)',
1114
+    )
1115
+    """"""
1085 1116
 
1086 1117
     EXPORT_VAULT_FORMAT_METAVAR_FMT = commented(
1087 1118
         '',
... ...
@@ -1785,6 +1816,16 @@ class WarnMsgTemplate(enum.Enum):
1785 1816
         'because a key is also set.',
1786 1817
     )
1787 1818
     """"""
1819
+    LEGACY_EDITOR_INTERFACE_NOTES_BACKUP = commented(
1820
+        '',
1821
+    )(
1822
+        'Warning message',
1823
+        'Using the vault(1)-compatible legacy editor interface, '
1824
+        'which does not allow aborting mid-edit.  '
1825
+        'A backup copy of the old notes was saved to {filename!r} '
1826
+        'to guard against editing mistakes.',
1827
+        flags='python-brace-format',
1828
+    )
1788 1829
     PASSPHRASE_NOT_NORMALIZED = commented(
1789 1830
         'The key is a (vault) configuration key, in JSONPath syntax, '
1790 1831
         'typically "$.global" for the global passphrase or '
... ...
@@ -593,6 +593,15 @@ def derivepassphrase_export_vault(
593 593
     ),
594 594
     cls=cli_machinery.CompatibilityOption,
595 595
 )
596
+@click.option(
597
+    '--modern-editor-interface/--vault-legacy-editor-interface',
598
+    'modern_editor_interface',
599
+    default=False,
600
+    help=_msg.TranslatedString(
601
+        _msg.Label.DERIVEPASSPHRASE_VAULT_EDITOR_INTERFACE_HELP_TEXT
602
+    ),
603
+    cls=cli_machinery.CompatibilityOption,
604
+)
596 605
 @cli_machinery.version_option
597 606
 @cli_machinery.color_forcing_pseudo_option
598 607
 @cli_machinery.standard_logging_options
... ...
@@ -629,6 +638,7 @@ def derivepassphrase_vault(  # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915
629 638
     overwrite_config: bool = False,
630 639
     unset_settings: Sequence[str] = (),
631 640
     export_as: Literal['json', 'sh'] = 'json',
641
+    modern_editor_interface: bool = False,
632 642
 ) -> None:
633 643
     """Derive a passphrase using the vault(1) derivation scheme.
634 644
 
... ...
@@ -722,6 +732,12 @@ def derivepassphrase_vault(  # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915
722 732
             Command-line argument `--export-as`.  If given together with
723 733
             `--export`, selects the format to export the current
724 734
             configuration as: JSON ("json", default) or POSIX sh ("sh").
735
+        modern_editor_interface:
736
+            Command-line arguments `--modern-editor-interface` (True)
737
+            and `--vault-legacy-editor-interface` (False).  Controls
738
+            whether editing notes uses a modern editor interface
739
+            (supporting comments and aborting) or a vault(1)-compatible
740
+            legacy editor interface (WYSIWYG notes contents).
725 741
 
726 742
     """  # noqa: DOC501
727 743
     logger = logging.getLogger(PROG_NAME)
... ...
@@ -996,7 +1012,7 @@ def derivepassphrase_vault(  # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915
996 1012
             extra={'color': ctx.color},
997 1013
         )
998 1014
 
999
-    if delete_service_settings:
1015
+    if delete_service_settings:  # noqa: PLR1702
1000 1016
         assert service is not None
1001 1017
         configuration = get_config()
1002 1018
         if service in configuration['services']:
... ...
@@ -1396,15 +1412,55 @@ def derivepassphrase_vault(  # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915
1396 1412
                 notes_marker = _msg.TranslatedString(
1397 1413
                     _msg.Label.DERIVEPASSPHRASE_VAULT_NOTES_MARKER
1398 1414
                 )
1415
+                notes_legacy_instructions = _msg.TranslatedString(
1416
+                    _msg.Label.DERIVEPASSPHRASE_VAULT_NOTES_LEGACY_INSTRUCTION_TEXT
1417
+                )
1399 1418
                 old_notes_value = subtree.get('notes', '')
1419
+                if modern_editor_interface:
1400 1420
                     text = '\n'.join([
1401 1421
                         str(notes_instructions),
1402 1422
                         str(notes_marker),
1403 1423
                         old_notes_value,
1404 1424
                     ])
1425
+                else:
1426
+                    text = str(notes_legacy_instructions)
1405 1427
                 notes_value = click.edit(text=text, require_save=False)
1406 1428
                 assert notes_value is not None
1407
-                if notes_value.strip() != old_notes_value.strip():
1429
+                if (
1430
+                    not modern_editor_interface
1431
+                    and notes_value.strip() != old_notes_value.strip()
1432
+                ):
1433
+                    backup_file = cli_helpers.config_filename(
1434
+                        subsystem='notes backup'
1435
+                    )
1436
+                    backup_file.write_text(old_notes_value, encoding='UTF-8')
1437
+                    logger.warning(
1438
+                        _msg.TranslatedString(
1439
+                            _msg.WarnMsgTemplate.LEGACY_EDITOR_INTERFACE_NOTES_BACKUP,
1440
+                            filename=str(backup_file),
1441
+                        ),
1442
+                        extra={'color': ctx.color},
1443
+                    )
1444
+                    subtree['notes'] = notes_value.strip()
1445
+                elif (
1446
+                    modern_editor_interface
1447
+                    and notes_value.strip() != text.strip()
1448
+                ):
1449
+                    notes_lines = collections.deque(
1450
+                        notes_value.splitlines(True)  # noqa: FBT003
1451
+                    )
1452
+                    while notes_lines:
1453
+                        line = notes_lines.popleft()
1454
+                        if line.startswith(str(notes_marker)):
1455
+                            notes_value = ''.join(notes_lines)
1456
+                            break
1457
+                    else:
1458
+                        if not notes_value.strip():
1459
+                            err(
1460
+                                _msg.TranslatedString(
1461
+                                    _msg.ErrMsgTemplate.USER_ABORTED_EDIT
1462
+                                )
1463
+                            )
1408 1464
                     subtree['notes'] = notes_value.strip()
1409 1465
             put_config(configuration)
1410 1466
         else:
... ...
@@ -639,6 +639,8 @@ class Parametrize(types.SimpleNamespace):
639 639
                     '--merge-existing',
640 640
                     '--unset',
641 641
                     '--export-as',
642
+                    '--modern-editor-interface',
643
+                    '--vault-legacy-editor-interface',
642 644
                 }),
643 645
                 id='derivepassphrase-vault',
644 646
             ),
... ...
@@ -2468,6 +2470,15 @@ class TestCLI:
2468 2470
             'expected error exit and known error message'
2469 2471
         )
2470 2472
 
2473
+    @pytest.mark.parametrize(
2474
+        'modern_editor_interface', [False, True], ids=['legacy', 'modern']
2475
+    )
2476
+    @hypothesis.settings(
2477
+        suppress_health_check=[
2478
+            *hypothesis.settings().suppress_health_check,
2479
+            hypothesis.HealthCheck.function_scoped_fixture,
2480
+        ],
2481
+    )
2471 2482
     @hypothesis.given(
2472 2483
         notes=strategies.text(
2473 2484
             strategies.characters(
... ...
@@ -2479,6 +2490,8 @@ class TestCLI:
2479 2490
     )
2480 2491
     def test_220_edit_notes_successfully(
2481 2492
         self,
2493
+        caplog: pytest.LogCaptureFixture,
2494
+        modern_editor_interface: bool,
2482 2495
         notes: str,
2483 2496
     ) -> None:
2484 2497
         """Editing notes works."""
... ...
@@ -2487,6 +2500,8 @@ class TestCLI:
2487 2500
 # - - - - - >8 - - - - - >8 - - - - - >8 - - - - - >8 - - - - -
2488 2501
 {notes}
2489 2502
 """
2503
+        # Reset caplog between hypothesis runs.
2504
+        caplog.clear()
2490 2505
         runner = click.testing.CliRunner(mix_stderr=False)
2491 2506
         # TODO(the-13th-letter): Rewrite using parenthesized
2492 2507
         # with-statements.
... ...
@@ -2503,46 +2518,62 @@ class TestCLI:
2503 2518
                     },
2504 2519
                 )
2505 2520
             )
2521
+            notes_backup_file = cli_helpers.config_filename(
2522
+                subsystem='notes backup'
2523
+            )
2524
+            notes_backup_file.write_text(
2525
+                'These backup notes are left over from the previous session.',
2526
+                encoding='UTF-8',
2527
+            )
2506 2528
             monkeypatch.setattr(click, 'edit', lambda *_a, **_kw: edit_result)
2507 2529
             result_ = runner.invoke(
2508 2530
                 cli.derivepassphrase_vault,
2509
-                ['--config', '--notes', '--', 'sv'],
2531
+                [
2532
+                    '--config',
2533
+                    '--notes',
2534
+                    '--modern-editor-interface'
2535
+                    if modern_editor_interface
2536
+                    else '--vault-legacy-editor-interface',
2537
+                    '--',
2538
+                    'sv',
2539
+                ],
2510 2540
                 catch_exceptions=False,
2511 2541
             )
2512 2542
             result = tests.ReadableResult.parse(result_)
2513
-            assert result.clean_exit(empty_stderr=True), 'expected clean exit'
2543
+            assert result.clean_exit(), 'expected clean exit'
2544
+            assert all(map(is_warning_line, result.stderr.splitlines(True)))
2545
+            assert modern_editor_interface or tests.warning_emitted(
2546
+                'Using the vault(1)-compatible legacy editor interface, '
2547
+                'which does not allow aborting mid-edit.  '
2548
+                'A backup copy of the old notes was saved',
2549
+                caplog.record_tuples,
2550
+            ), 'expected known warning message in stderr'
2551
+            assert (
2552
+                modern_editor_interface
2553
+                or notes_backup_file.read_text(encoding='UTF-8')
2554
+                == 'Contents go here'
2555
+            )
2514 2556
             with cli_helpers.config_filename(subsystem='vault').open(
2515 2557
                 encoding='UTF-8'
2516 2558
             ) as infile:
2517 2559
                 config = json.load(infile)
2518 2560
             assert config == {
2519 2561
                 'global': {'phrase': 'abc'},
2520
-                'services': {'sv': {'notes': edit_result.strip()}},
2562
+                'services': {
2563
+                    'sv': {
2564
+                        'notes': notes.strip()
2565
+                        if modern_editor_interface
2566
+                        else edit_result.strip()
2567
+                    }
2568
+                },
2521 2569
             }
2522 2570
 
2523 2571
     @pytest.mark.parametrize(
2524
-        'edit_func_name',
2572
+        ['edit_func_name', 'modern_editor_interface'],
2525 2573
         [
2526
-            pytest.param(
2527
-                'empty',
2528
-                marks=[
2529
-                    pytest.mark.xfail(reason='incompatibility with vault(1)')
2530
-                ],
2531
-            ),
2532
-            'space',
2533
-        ],
2534
-    )
2535
-    # Skip the "target", "shrink" and "explain" phases on this test because
2536
-    # one of its parametrizations is marked xfail, and we don't want to do
2537
-    # a lot of extra work that will be thrown away anyway.
2538
-    #
2539
-    # TODO(the-13th-letter): remove this settings decorator once the
2540
-    # xfail'ing parametrization is fixed.
2541
-    @hypothesis.settings(
2542
-        phases=[
2543
-            hypothesis.Phase.explicit,
2544
-            hypothesis.Phase.reuse,
2545
-            hypothesis.Phase.generate,
2574
+            pytest.param('empty', True, id='empty'),
2575
+            pytest.param('space', False, id='space-legacy'),
2576
+            pytest.param('space', True, id='space-modern'),
2546 2577
         ],
2547 2578
     )
2548 2579
     @hypothesis.given(
... ...
@@ -2557,16 +2588,18 @@ class TestCLI:
2557 2588
     def test_221_edit_notes_noop(
2558 2589
         self,
2559 2590
         edit_func_name: Literal['empty', 'space'],
2591
+        modern_editor_interface: bool,
2560 2592
         notes: str,
2561 2593
     ) -> None:
2562 2594
         """Abandoning edited notes works."""
2563 2595
 
2564
-        def empty(text: str, *_args: Any, **_kwargs: Any) -> None:
2596
+        def empty(text: str, *_args: Any, **_kwargs: Any) -> str:
2565 2597
             del text
2598
+            return ''
2566 2599
 
2567 2600
         def space(text: str, *_args: Any, **_kwargs: Any) -> str:
2568 2601
             del text
2569
-            return '       ' + notes + '\n\n\n\n\n\n'
2602
+            return '       ' + notes.strip() + '\n\n\n\n\n\n'
2570 2603
 
2571 2604
         edit_funcs = {'empty': empty, 'space': space}
2572 2605
         runner = click.testing.CliRunner(mix_stderr=False)
... ...
@@ -2581,30 +2614,60 @@ class TestCLI:
2581 2614
                     runner=runner,
2582 2615
                     vault_config={
2583 2616
                         'global': {'phrase': 'abc'},
2584
-                        'services': {'sv': {'notes': notes}},
2617
+                        'services': {'sv': {'notes': notes.strip()}},
2585 2618
                     },
2586 2619
                 )
2587 2620
             )
2588
-
2621
+            notes_backup_file = cli_helpers.config_filename(
2622
+                subsystem='notes backup'
2623
+            )
2624
+            notes_backup_file.write_text(
2625
+                'These backup notes are left over from the previous session.',
2626
+                encoding='UTF-8',
2627
+            )
2589 2628
             monkeypatch.setattr(click, 'edit', edit_funcs[edit_func_name])
2590 2629
             result_ = runner.invoke(
2591 2630
                 cli.derivepassphrase_vault,
2592
-                ['--config', '--notes', '--', 'sv'],
2631
+                [
2632
+                    '--config',
2633
+                    '--notes',
2634
+                    '--modern-editor-interface'
2635
+                    if modern_editor_interface
2636
+                    else '--vault-legacy-editor-interface',
2637
+                    '--',
2638
+                    'sv',
2639
+                ],
2593 2640
                 catch_exceptions=False,
2594 2641
             )
2595 2642
             result = tests.ReadableResult.parse(result_)
2596
-            assert result.clean_exit(empty_stderr=True), 'expected clean exit'
2643
+            assert result.clean_exit(empty_stderr=True) or result.error_exit(
2644
+                error='the user aborted the request'
2645
+            ), 'expected clean exit'
2646
+            assert (
2647
+                modern_editor_interface
2648
+                or notes_backup_file.read_text(encoding='UTF-8')
2649
+                == 'These backup notes are left over from the previous session.'
2650
+            )
2597 2651
             with cli_helpers.config_filename(subsystem='vault').open(
2598 2652
                 encoding='UTF-8'
2599 2653
             ) as infile:
2600 2654
                 config = json.load(infile)
2601 2655
             assert config == {
2602 2656
                 'global': {'phrase': 'abc'},
2603
-                'services': {'sv': {'notes': notes}},
2657
+                'services': {'sv': {'notes': notes.strip()}},
2604 2658
             }
2605 2659
 
2606 2660
     # TODO(the-13th-letter): Keep this behavior or not, with or without
2607 2661
     # warning?
2662
+    @pytest.mark.parametrize(
2663
+        'modern_editor_interface', [False, True], ids=['legacy', 'modern']
2664
+    )
2665
+    @hypothesis.settings(
2666
+        suppress_health_check=[
2667
+            *hypothesis.settings().suppress_health_check,
2668
+            hypothesis.HealthCheck.function_scoped_fixture,
2669
+        ],
2670
+    )
2608 2671
     @hypothesis.given(
2609 2672
         notes=strategies.text(
2610 2673
             strategies.characters(
... ...
@@ -2616,6 +2679,8 @@ class TestCLI:
2616 2679
     )
2617 2680
     def test_222_edit_notes_marker_removed(
2618 2681
         self,
2682
+        caplog: pytest.LogCaptureFixture,
2683
+        modern_editor_interface: bool,
2619 2684
         notes: str,
2620 2685
     ) -> None:
2621 2686
         """Removing the notes marker still saves the notes.
... ...
@@ -2627,6 +2692,8 @@ class TestCLI:
2627 2692
             cli_messages.Label.DERIVEPASSPHRASE_VAULT_NOTES_MARKER
2628 2693
         )
2629 2694
         hypothesis.assume(str(notes_marker) not in notes.strip())
2695
+        # Reset caplog between hypothesis runs.
2696
+        caplog.clear()
2630 2697
         runner = click.testing.CliRunner(mix_stderr=False)
2631 2698
         # TODO(the-13th-letter): Rewrite using parenthesized
2632 2699
         # with-statements.
... ...
@@ -2643,14 +2710,43 @@ class TestCLI:
2643 2710
                     },
2644 2711
                 )
2645 2712
             )
2713
+            notes_backup_file = cli_helpers.config_filename(
2714
+                subsystem='notes backup'
2715
+            )
2716
+            notes_backup_file.write_text(
2717
+                'These backup notes are left over from the previous session.',
2718
+                encoding='UTF-8',
2719
+            )
2646 2720
             monkeypatch.setattr(click, 'edit', lambda *_a, **_kw: notes)
2647 2721
             result_ = runner.invoke(
2648 2722
                 cli.derivepassphrase_vault,
2649
-                ['--config', '--notes', '--', 'sv'],
2723
+                [
2724
+                    '--config',
2725
+                    '--notes',
2726
+                    '--modern-editor-interface'
2727
+                    if modern_editor_interface
2728
+                    else '--vault-legacy-editor-interface',
2729
+                    '--',
2730
+                    'sv',
2731
+                ],
2650 2732
                 catch_exceptions=False,
2651 2733
             )
2652 2734
             result = tests.ReadableResult.parse(result_)
2653
-            assert result.clean_exit(empty_stderr=True), 'expected clean exit'
2735
+            assert result.clean_exit(), 'expected clean exit'
2736
+            assert not result.stderr or all(
2737
+                map(is_warning_line, result.stderr.splitlines(True))
2738
+            )
2739
+            assert not caplog.record_tuples or tests.warning_emitted(
2740
+                'Using the vault(1)-compatible legacy editor interface, '
2741
+                'which does not allow aborting mid-edit.  '
2742
+                'A backup copy of the old notes was saved',
2743
+                caplog.record_tuples,
2744
+            ), 'expected known warning message in stderr'
2745
+            assert (
2746
+                modern_editor_interface
2747
+                or notes_backup_file.read_text(encoding='UTF-8')
2748
+                == 'Contents go here'
2749
+            )
2654 2750
             with cli_helpers.config_filename(subsystem='vault').open(
2655 2751
                 encoding='UTF-8'
2656 2752
             ) as infile:
... ...
@@ -2660,20 +2756,6 @@ class TestCLI:
2660 2756
                 'services': {'sv': {'notes': notes.strip()}},
2661 2757
             }
2662 2758
 
2663
-    @pytest.mark.xfail(reason='incompatibility with vault(1)')
2664
-    # Skip the "target", "shrink" and "explain" phases on this test because
2665
-    # this test is marked xfail, and we don't want to do a lot of extra work
2666
-    # that will be thrown away anyway.
2667
-    #
2668
-    # TODO(the-13th-letter): remove this settings decorator once the xfail
2669
-    # is fixed.
2670
-    @hypothesis.settings(
2671
-        phases=[
2672
-            hypothesis.Phase.explicit,
2673
-            hypothesis.Phase.reuse,
2674
-            hypothesis.Phase.generate,
2675
-        ],
2676
-    )
2677 2759
     @hypothesis.given(
2678 2760
         notes=strategies.text(
2679 2761
             strategies.characters(
... ...
@@ -2687,7 +2769,11 @@ class TestCLI:
2687 2769
         self,
2688 2770
         notes: str,
2689 2771
     ) -> None:
2690
-        """Aborting editing notes works."""
2772
+        """Aborting editing notes works.
2773
+
2774
+        Aborting is only supported with the modern editor interface.
2775
+
2776
+        """
2691 2777
         runner = click.testing.CliRunner(mix_stderr=False)
2692 2778
         # TODO(the-13th-letter): Rewrite using parenthesized
2693 2779
         # with-statements.
... ...
@@ -2707,7 +2793,13 @@ class TestCLI:
2707 2793
             monkeypatch.setattr(click, 'edit', lambda *_a, **_kw: '')
2708 2794
             result_ = runner.invoke(
2709 2795
                 cli.derivepassphrase_vault,
2710
-                ['--config', '--notes', '--', 'sv'],
2796
+                [
2797
+                    '--config',
2798
+                    '--notes',
2799
+                    '--modern-editor-interface',
2800
+                    '--',
2801
+                    'sv',
2802
+                ],
2711 2803
                 catch_exceptions=False,
2712 2804
             )
2713 2805
             result = tests.ReadableResult.parse(result_)
... ...
@@ -2723,11 +2815,14 @@ class TestCLI:
2723 2815
                 'services': {'sv': {'notes': notes.strip()}},
2724 2816
             }
2725 2817
 
2726
-    @pytest.mark.xfail(reason='incompatibility with vault(1)')
2727 2818
     def test_223a_edit_empty_notes_abort(
2728 2819
         self,
2729 2820
     ) -> None:
2730
-        """Aborting editing notes works even if no notes are stored yet."""
2821
+        """Aborting editing notes works even if no notes are stored yet.
2822
+
2823
+        Aborting is only supported with the modern editor interface.
2824
+
2825
+        """
2731 2826
         runner = click.testing.CliRunner(mix_stderr=False)
2732 2827
         # TODO(the-13th-letter): Rewrite using parenthesized
2733 2828
         # with-statements.
... ...
@@ -2747,7 +2842,13 @@ class TestCLI:
2747 2842
             monkeypatch.setattr(click, 'edit', lambda *_a, **_kw: '')
2748 2843
             result_ = runner.invoke(
2749 2844
                 cli.derivepassphrase_vault,
2750
-                ['--config', '--notes', '--', 'sv'],
2845
+                [
2846
+                    '--config',
2847
+                    '--notes',
2848
+                    '--modern-editor-interface',
2849
+                    '--',
2850
+                    'sv',
2851
+                ],
2751 2852
                 catch_exceptions=False,
2752 2853
             )
2753 2854
             result = tests.ReadableResult.parse(result_)
... ...
@@ -2763,6 +2864,9 @@ class TestCLI:
2763 2864
                 'services': {},
2764 2865
             }
2765 2866
 
2867
+    @pytest.mark.parametrize(
2868
+        'modern_editor_interface', [False, True], ids=['legacy', 'modern']
2869
+    )
2766 2870
     @hypothesis.settings(
2767 2871
         suppress_health_check=[
2768 2872
             *hypothesis.settings().suppress_health_check,
... ...
@@ -2780,6 +2884,7 @@ class TestCLI:
2780 2884
     def test_223b_edit_notes_fail_config_option_missing(
2781 2885
         self,
2782 2886
         caplog: pytest.LogCaptureFixture,
2887
+        modern_editor_interface: bool,
2783 2888
         notes: str,
2784 2889
     ) -> None:
2785 2890
         """Editing notes fails (and warns) if `--config` is missing."""
... ...
@@ -2810,10 +2915,24 @@ class TestCLI:
2810 2915
             def raiser(*_args: Any, **_kwargs: Any) -> NoReturn:
2811 2916
                 pytest.fail(EDIT_ATTEMPTED)
2812 2917
 
2918
+            notes_backup_file = cli_helpers.config_filename(
2919
+                subsystem='notes backup'
2920
+            )
2921
+            notes_backup_file.write_text(
2922
+                'These backup notes are left over from the previous session.',
2923
+                encoding='UTF-8',
2924
+            )
2813 2925
             monkeypatch.setattr(click, 'edit', raiser)
2814 2926
             result_ = runner.invoke(
2815 2927
                 cli.derivepassphrase_vault,
2816
-                ['--notes', '--', DUMMY_SERVICE],
2928
+                [
2929
+                    '--notes',
2930
+                    '--modern-editor-interface'
2931
+                    if modern_editor_interface
2932
+                    else '--vault-legacy-editor-interface',
2933
+                    '--',
2934
+                    DUMMY_SERVICE,
2935
+                ],
2817 2936
                 catch_exceptions=False,
2818 2937
             )
2819 2938
             result = tests.ReadableResult.parse(result_)
... ...
@@ -2832,6 +2951,11 @@ class TestCLI:
2832 2951
                 'No notes will be edited.',
2833 2952
                 caplog.record_tuples,
2834 2953
             ), 'expected known warning message in stderr'
2954
+            assert (
2955
+                modern_editor_interface
2956
+                or notes_backup_file.read_text(encoding='UTF-8')
2957
+                == 'These backup notes are left over from the previous session.'
2958
+            )
2835 2959
             with cli_helpers.config_filename(subsystem='vault').open(
2836 2960
                 encoding='UTF-8'
2837 2961
             ) as infile:
2838 2962