Overhaul the state machine for testing vault configuration imports
Marco Ricci

Marco Ricci commited on 2024-12-12 12:03:30
Zeige 2 geänderte Dateien mit 205 Einfügungen und 209 Löschungen.


Redesign the state machine for testing vault configuration imports, and
the `hypothesis` generation strategies for vault configurations, to
overcome the following problems with the old design:

 1. Data generation of vault configurations needs many redraws, because
    we express dependencies by rejecting the whole tuple of properties
    if it doesn't match.  We could instead draw the independent
    properties, and conditional on their values, draw the dependent
    ones; cf. Gibbs sampling vs. rejection sampling.

 2. `hypothesis` implements state machine rule selection as drawing from
    a list of eligible rules.  This list must remain static during
    redrawing and backtracking, lest `hypothesis` complains the test is
    flaky.  While it isn't expressly forbidden to store state manually
    in the state machine, the old design made the list of eligible
    rules dependent on the implicit machine state and on the generated
    values, in a way that led to flakiness.

Fix these issues by doing the following:

  - Rename the class from `ConfigMergingStateMachine` to
    `ConfigManagementStateMachine`, because it's no longer just about
    merging configurations.

  - Implement strategies to build a full vault configuration from
    multiple full settings configurations, and a partial settings
    configuration from a full settings configuration.  Draw
    interactively, first the independent properties, then the dependent
    ones.  This tackles issue (1).

  - Overhaul the state machine, effectively rewriting it from scratch,
    by managing all state in `hypothesis` bundles.  Ensure the bundles
    are filled at initialization, and that each (non-bookkeeping) rule
    begins with storing the configuration and ends with checking and
    returning the transformed configuration.  Filter the values drawn
    from the bundles, instead of filtering the rule set based on the
    machine's current state.  This tackles issue (2) above.

The resulting state machine should have an acceptable drawing success
rate (without needing to override the `filter_too_much` `hypothesis`
health check), at the cost of slow(ish) drawing speed and slow(er)
execution speed because the configuration must be replayed onto disk
during each step.  On the other hand, the single steps now are more
repeatable and more independent of each other.

This implementation makes use of several top-level functions and
constants, outside of the state machine class.  Attempting to use static
and class methods instead leads to messy workarounds because of the name
resolution rules and because of mismatches in the calling conventions of
static and class methods during class definition vs. during execution
time.  In the end, the "top-level functions and constants" version is
the most straight-forward to read.
... ...
@@ -333,13 +333,13 @@ def _test_config_ids(val: VaultTestConfig) -> Any:  # pragma: no cover
333 333
 
334 334
 @strategies.composite
335 335
 def vault_full_service_config(draw: strategies.DrawFn) -> dict[str, int]:
336
+    repeat = draw(strategies.integers(min_value=0, max_value=10))
336 337
     lower = draw(strategies.integers(min_value=0, max_value=10))
337 338
     upper = draw(strategies.integers(min_value=0, max_value=10))
338 339
     number = draw(strategies.integers(min_value=0, max_value=10))
339
-    space = draw(strategies.integers(min_value=0, max_value=10))
340
+    space = draw(strategies.integers(min_value=0, max_value=repeat))
340 341
     dash = draw(strategies.integers(min_value=0, max_value=10))
341 342
     symbol = draw(strategies.integers(min_value=0, max_value=10))
342
-    repeat = draw(strategies.integers(min_value=0, max_value=10))
343 343
     length = draw(
344 344
         strategies.integers(
345 345
             min_value=max(1, lower + upper + number + space + dash + symbol),
... ...
@@ -13,7 +13,7 @@ import os
13 13
 import shutil
14 14
 import socket
15 15
 import warnings
16
-from typing import TYPE_CHECKING, cast
16
+from typing import TYPE_CHECKING
17 17
 
18 18
 import click.testing
19 19
 import hypothesis
... ...
@@ -2327,7 +2327,79 @@ class TestCLITransition:
2327 2327
         ), 'expected known warning message in stderr'
2328 2328
 
2329 2329
 
2330
-class ConfigMergingStateMachine(stateful.RuleBasedStateMachine):
2330
+_known_services = (DUMMY_SERVICE, 'email', 'bank', 'work')
2331
+_valid_properties = (
2332
+    'length',
2333
+    'repeat',
2334
+    'upper',
2335
+    'lower',
2336
+    'number',
2337
+    'space',
2338
+    'dash',
2339
+    'symbol',
2340
+)
2341
+
2342
+
2343
+def _build_reduced_vault_config_settings(
2344
+    config: _types.VaultConfigServicesSettings,
2345
+    keys_to_purge: frozenset[str],
2346
+) -> _types.VaultConfigServicesSettings:
2347
+    config2 = copy.deepcopy(config)
2348
+    for key in keys_to_purge:
2349
+        config2.pop(key, None)  # type: ignore[misc]
2350
+    return config2
2351
+
2352
+
2353
+_services_strategy = strategies.builds(
2354
+    _build_reduced_vault_config_settings,
2355
+    tests.vault_full_service_config(),
2356
+    strategies.sets(
2357
+        strategies.sampled_from(_valid_properties),
2358
+        max_size=7,
2359
+    ),
2360
+)
2361
+
2362
+
2363
+def _assemble_config(
2364
+    global_data: _types.VaultConfigGlobalSettings,
2365
+    service_data: list[tuple[str, _types.VaultConfigServicesSettings]],
2366
+) -> _types.VaultConfig:
2367
+    services_dict = dict(service_data)
2368
+    return (
2369
+        {'global': global_data, 'services': services_dict}
2370
+        if global_data
2371
+        else {'services': services_dict}
2372
+    )
2373
+
2374
+
2375
+@strategies.composite
2376
+def _draw_service_name_and_data(
2377
+    draw: hypothesis.strategies.DrawFn,
2378
+    num_entries: int,
2379
+) -> tuple[tuple[str, _types.VaultConfigServicesSettings], ...]:
2380
+    possible_services = list(_known_services)
2381
+    selected_services: list[str] = []
2382
+    for _ in range(num_entries):
2383
+        selected_services.append(
2384
+            draw(strategies.sampled_from(possible_services))
2385
+        )
2386
+        possible_services.remove(selected_services[-1])
2387
+    return tuple(
2388
+        (service, draw(_services_strategy)) for service in selected_services
2389
+    )
2390
+
2391
+
2392
+_vault_full_config = strategies.builds(
2393
+    _assemble_config,
2394
+    _services_strategy,
2395
+    strategies.integers(
2396
+        min_value=2,
2397
+        max_value=4,
2398
+    ).flatmap(_draw_service_name_and_data),
2399
+)
2400
+
2401
+
2402
+class ConfigManagementStateMachine(stateful.RuleBasedStateMachine):
2331 2403
     def __init__(self) -> None:
2332 2404
         super().__init__()
2333 2405
         self.runner = click.testing.CliRunner(mix_stderr=False)
... ...
@@ -2342,165 +2414,63 @@ class ConfigMergingStateMachine(stateful.RuleBasedStateMachine):
2342 2414
                 config={'services': {}},
2343 2415
             )
2344 2416
         )
2345
-        self.current_config = cli._load_config()
2346
-
2347
-    known_services = stateful.Bundle('known_services')
2348
-    settings = stateful.Bundle('settings')
2349
-    configurations = stateful.Bundle('configurations')
2350 2417
 
2351
-    @stateful.initialize(target=configurations)
2352
-    def init_empty_configuration(self) -> _types.VaultConfig:
2353
-        return copy.deepcopy(self.current_config)
2354
-
2355
-    @stateful.initialize(target=configurations)
2356
-    def init_standard_testing_configuration(self) -> _types.VaultConfig:
2357
-        return cast(
2358
-            _types.VaultConfig,
2359
-            {
2360
-                'services': {
2361
-                    DUMMY_SERVICE: copy.deepcopy(DUMMY_CONFIG_SETTINGS)
2362
-                }
2363
-            },
2364
-        )
2418
+    setting = stateful.Bundle('setting')
2419
+    configuration = stateful.Bundle('configuration')
2365 2420
 
2366 2421
     @stateful.initialize(
2367
-        target=known_services,
2368
-        service_names=strategies.lists(
2369
-            strategies.text(
2370
-                strategies.characters(min_codepoint=32, max_codepoint=126),
2371
-                min_size=1,
2372
-                max_size=50,
2373
-            ),
2374
-            min_size=10,
2375
-            max_size=10,
2376
-            unique=True,
2377
-        ),
2378
-    )
2379
-    def init_random_service_names(
2380
-        self, service_names: list[str]
2381
-    ) -> Iterable[str]:
2382
-        return stateful.multiple(*service_names)
2383
-
2384
-    # Don't include key or phrase settings here.  While easy to
2385
-    # implement when manipulating the stored config directly, the
2386
-    # command-line interface for changing the passphrase and key values
2387
-    # is not straight-forward, and key values require a running SSH
2388
-    # agent and the key to be loaded.
2389
-    @stateful.initialize(
2390
-        target=settings,
2391
-        settings_list=strategies.lists(
2392
-            tests.vault_full_service_config(),
2393
-            min_size=5,
2394
-            max_size=5,
2395
-            unique_by=lambda obj: json.dumps(obj, sort_keys=True),
2422
+        target=configuration,
2423
+        configs=strategies.lists(
2424
+            _vault_full_config,
2425
+            min_size=4,
2426
+            max_size=4,
2396 2427
         ),
2397 2428
     )
2398
-    def init_random_full_settings(
2399
-        self, settings_list: list[_types.VaultConfigGlobalSettings]
2400
-    ) -> Iterable[_types.VaultConfigGlobalSettings]:
2401
-        return stateful.multiple(*settings_list)
2402
-
2403
-    @staticmethod
2404
-    def build_reduced_vault_config_settings(
2405
-        config: _types.VaultConfigGlobalSettings,
2406
-        keys_to_purge: frozenset[str],
2407
-    ) -> _types.VaultConfigGlobalSettings:
2408
-        config2 = copy.deepcopy(config)
2409
-        for key in keys_to_purge:
2410
-            config2.pop(key, None)  # type: ignore[misc]
2411
-        return config2
2429
+    def declare_initial_configs(
2430
+        self,
2431
+        configs: Iterable[_types.VaultConfig],
2432
+    ) -> Iterable[_types.VaultConfig]:
2433
+        return stateful.multiple(*configs)
2412 2434
 
2413
-    # See comment on `init_random_full_settings`.
2414 2435
     @stateful.initialize(
2415
-        target=settings,
2416
-        settings_list=strategies.lists(
2417
-            strategies.builds(
2418
-                build_reduced_vault_config_settings,
2419
-                tests.vault_full_service_config(),
2420
-                strategies.sets(
2421
-                    strategies.sampled_from([
2422
-                        'length',
2423
-                        'repeat',
2424
-                        'upper',
2425
-                        'lower',
2426
-                        'number',
2427
-                        'space',
2428
-                        'dash',
2429
-                        'symbol',
2430
-                    ]),
2431
-                    max_size=7,
2432
-                ),
2433
-            ),
2434
-            min_size=5,
2435
-            max_size=5,
2436
-            unique_by=lambda obj: json.dumps(obj, sort_keys=True),
2437
-        ).filter(bool),
2436
+        target=setting,
2437
+        config=_vault_full_config,
2438 2438
     )
2439
-    def init_random_partial_settings(
2440
-        self, settings_list: list[_types.VaultConfigGlobalSettings]
2441
-    ) -> Iterable[_types.VaultConfigGlobalSettings]:
2442
-        return stateful.multiple(*settings_list)
2443
-
2444
-    @stateful.invariant()
2445
-    def check_consistency_of_configs(self) -> None:
2446
-        _types.clean_up_falsy_vault_config_values(self.current_config)
2447
-        assert self.current_config == cli._load_config()
2448
-
2449
-    @stateful.rule(
2450
-        target=settings,
2451
-        settings_obj=tests.vault_full_service_config(),
2452
-    )
2453
-    def prepare_settings(
2454
-        self, settings_obj: dict[str, int]
2455
-    ) -> _types.VaultConfigGlobalSettings:
2456
-        return cast(_types.VaultConfigGlobalSettings, settings_obj.copy())
2457
-
2458
-    @stateful.rule(
2459
-        target=known_services,
2460
-        name=strategies.text(
2461
-            strategies.characters(min_codepoint=32, max_codepoint=126),
2462
-            min_size=1,
2463
-            max_size=50,
2464
-        ),
2439
+    def extract_initial_settings(
2440
+        self,
2441
+        config: _types.VaultConfig,
2442
+    ) -> Iterable[_types.VaultConfigServicesSettings]:
2443
+        return stateful.multiple(
2444
+            *map(copy.deepcopy, config['services'].values())
2465 2445
         )
2466
-    def prepare_service_name(self, name: str) -> str:
2467
-        return name
2468 2446
 
2469 2447
     @stateful.rule(
2470
-        target=configurations,
2471
-        settings_obj=stateful.consumes(settings),
2448
+        target=configuration,
2449
+        config=_vault_full_config,
2472 2450
     )
2473
-    def prepare_global_config(
2451
+    def declare_config(
2474 2452
         self,
2475
-        settings_obj: dict[str, int],
2453
+        config: _types.VaultConfig,
2476 2454
     ) -> _types.VaultConfig:
2477
-        return {
2478
-            'global': cast(_types.VaultConfigGlobalSettings, settings_obj),
2479
-            'services': {},
2480
-        }
2455
+        return config
2481 2456
 
2482 2457
     @stateful.rule(
2483
-        target=configurations,
2484
-        service=known_services,
2485
-        settings_obj=stateful.consumes(settings),
2458
+        target=setting,
2459
+        config=_vault_full_config,
2486 2460
     )
2487
-    def prepare_service_config(
2461
+    def extract_settings(
2488 2462
         self,
2489
-        service: str,
2490
-        settings_obj: dict[str, int],
2491
-    ) -> _types.VaultConfig:
2492
-        return {
2493
-            'services': {
2494
-                service: cast(
2495
-                    _types.VaultConfigServicesSettings, settings_obj
2496
-                ),
2497
-            },
2498
-        }
2463
+        config: _types.VaultConfig,
2464
+    ) -> Iterable[_types.VaultConfigServicesSettings]:
2465
+        return stateful.multiple(
2466
+            *map(copy.deepcopy, config['services'].values())
2467
+        )
2499 2468
 
2500 2469
     @staticmethod
2501 2470
     def fold_configs(
2502 2471
         c1: _types.VaultConfig, c2: _types.VaultConfig
2503 2472
     ) -> _types.VaultConfig:
2473
+        """Fold `c1` into `c2`, overriding the latter."""
2504 2474
         new_global_dict = c1.get('global', c2.get('global'))
2505 2475
         if new_global_dict is not None:
2506 2476
             return {
... ...
@@ -2512,30 +2482,22 @@ class ConfigMergingStateMachine(stateful.RuleBasedStateMachine):
2512 2482
         }
2513 2483
 
2514 2484
     @stateful.rule(
2515
-        target=configurations,
2516
-        config_base=stateful.consumes(configurations),
2517
-        config_folded=stateful.consumes(configurations),
2518
-    )
2519
-    def fold_configuration_into(
2520
-        self,
2521
-        config_base: _types.VaultConfig,
2522
-        config_folded: _types.VaultConfig,
2523
-    ) -> _types.VaultConfig:
2524
-        return self.fold_configs(config_folded, config_base)
2525
-
2526
-    @stateful.rule(
2527
-        settings_obj=stateful.consumes(settings),
2485
+        target=configuration,
2486
+        config=configuration,
2487
+        setting=setting.filter(bool),
2528 2488
         overwrite=strategies.booleans(),
2529 2489
     )
2530 2490
     def set_globals(
2531 2491
         self,
2532
-        settings_obj: _types.VaultConfigGlobalSettings,
2492
+        config: _types.VaultConfig,
2493
+        setting: _types.VaultConfigGlobalSettings,
2533 2494
         overwrite: bool,
2534
-    ) -> None:
2495
+    ) -> _types.VaultConfig:
2496
+        cli._save_config(config)
2535 2497
         if overwrite:
2536
-            self.current_config['global'] = {}
2537
-        self.current_config.setdefault('global', {}).update(settings_obj)
2538
-        assert _types.is_vault_config(self.current_config)
2498
+            config['global'] = {}
2499
+        config.setdefault('global', {}).update(setting)
2500
+        assert _types.is_vault_config(config)
2539 2501
         # NOTE: This relies on settings_obj containing only the keys
2540 2502
         # "length", "repeat", "upper", "lower", "number", "space",
2541 2503
         # "dash" and "symbol".
... ...
@@ -2545,38 +2507,37 @@ class ConfigMergingStateMachine(stateful.RuleBasedStateMachine):
2545 2507
                 '--config',
2546 2508
                 '--overwrite-existing' if overwrite else '--merge-existing',
2547 2509
             ]
2548
-            + [f'--{key}={value}' for key, value in settings_obj.items()],
2510
+            + [
2511
+                f'--{key}={value}'
2512
+                for key, value in setting.items()
2513
+                if key in _valid_properties
2514
+            ],
2549 2515
             catch_exceptions=False,
2550 2516
         )
2551 2517
         result = tests.ReadableResult.parse(_result)
2552 2518
         assert result.clean_exit(empty_stderr=False)
2519
+        assert cli._load_config() == config
2520
+        return config
2553 2521
 
2554
-    # No check for whether the service settings currently exist.  This
2555
-    # may therefore actually "create" the settings, not merely "modify"
2556
-    # them.
2557
-    #
2558
-    # (There is no check because this appears to be hard or impossible
2559
-    # to express as a hypothesis strategy: it would depend on the
2560
-    # current state of the state machine instance.  This could be
2561
-    # circumvented with `hypothesis.assume`, but that may likely trigger
2562
-    # health check errors.)
2563 2522
     @stateful.rule(
2564
-        service=known_services,
2565
-        settings_obj=stateful.consumes(settings),
2523
+        target=configuration,
2524
+        config=configuration,
2525
+        service=strategies.sampled_from(_known_services),
2526
+        setting=setting.filter(bool),
2566 2527
         overwrite=strategies.booleans(),
2567 2528
     )
2568 2529
     def set_service(
2569 2530
         self,
2531
+        config: _types.VaultConfig,
2570 2532
         service: str,
2571
-        settings_obj: _types.VaultConfigServicesSettings,
2533
+        setting: _types.VaultConfigServicesSettings,
2572 2534
         overwrite: bool,
2573
-    ) -> None:
2535
+    ) -> _types.VaultConfig:
2536
+        cli._save_config(config)
2574 2537
         if overwrite:
2575
-            self.current_config['services'][service] = {}
2576
-        self.current_config['services'].setdefault(service, {}).update(
2577
-            settings_obj
2578
-        )
2579
-        assert _types.is_vault_config(self.current_config)
2538
+            config['services'][service] = {}
2539
+        config['services'].setdefault(service, {}).update(setting)
2540
+        assert _types.is_vault_config(config)
2580 2541
         # NOTE: This relies on settings_obj containing only the keys
2581 2542
         # "length", "repeat", "upper", "lower", "number", "space",
2582 2543
         # "dash" and "symbol".
... ...
@@ -2586,17 +2547,29 @@ class ConfigMergingStateMachine(stateful.RuleBasedStateMachine):
2586 2547
                 '--config',
2587 2548
                 '--overwrite-existing' if overwrite else '--merge-existing',
2588 2549
             ]
2589
-            + [f'--{key}={value}' for key, value in settings_obj.items()]
2550
+            + [
2551
+                f'--{key}={value}'
2552
+                for key, value in setting.items()
2553
+                if key in _valid_properties
2554
+            ]
2590 2555
             + ['--', service],
2591 2556
             catch_exceptions=False,
2592 2557
         )
2593 2558
         result = tests.ReadableResult.parse(_result)
2594 2559
         assert result.clean_exit(empty_stderr=False)
2560
+        assert cli._load_config() == config
2561
+        return config
2595 2562
 
2596
-    @stateful.precondition(lambda self: 'global' in self.current_config)
2597
-    @stateful.rule()
2598
-    def purge_global(self) -> None:
2599
-        self.current_config.pop('global')
2563
+    @stateful.rule(
2564
+        target=configuration,
2565
+        config=configuration.filter(lambda c: 'global' in c),
2566
+    )
2567
+    def purge_global(
2568
+        self,
2569
+        config: _types.VaultConfig,
2570
+    ) -> _types.VaultConfig:
2571
+        cli._save_config(config)
2572
+        config.pop('global')
2600 2573
         _result = self.runner.invoke(
2601 2574
             cli.derivepassphrase_vault,
2602 2575
             ['--delete-globals'],
... ...
@@ -2605,21 +2578,27 @@ class ConfigMergingStateMachine(stateful.RuleBasedStateMachine):
2605 2578
         )
2606 2579
         result = tests.ReadableResult.parse(_result)
2607 2580
         assert result.clean_exit(empty_stderr=False)
2581
+        assert cli._load_config() == config
2582
+        return config
2608 2583
 
2609
-    # No check for whether the service settings currently exist.  This
2610
-    # may therefore actually be almost a no-op, purging settings that
2611
-    # aren't set in the first place.
2612
-    #
2613
-    # (There is no check because this appears to be hard or impossible
2614
-    # to express as a hypothesis strategy: it would depend on the
2615
-    # current state of the state machine instance.  This could be
2616
-    # circumvented with `hypothesis.assume`, but that may likely trigger
2617
-    # health check errors.)
2618
-    @stateful.precondition(lambda self: bool(self.current_config['services']))
2619
-    @stateful.rule(service=stateful.consumes(known_services))
2620
-    def purge_service(self, service: str) -> None:
2621
-        ret = self.current_config['services'].pop(service, None)
2622
-        if ret is not None:
2584
+    @stateful.rule(
2585
+        target=configuration,
2586
+        config_and_service=configuration.filter(
2587
+            lambda c: len(c['services']) > 1
2588
+        ).flatmap(
2589
+            lambda c: strategies.tuples(
2590
+                strategies.just(c),
2591
+                strategies.sampled_from(tuple(c['services'].keys())),
2592
+            )
2593
+        ),
2594
+    )
2595
+    def purge_service(
2596
+        self,
2597
+        config_and_service: tuple[_types.VaultConfig, str],
2598
+    ) -> _types.VaultConfig:
2599
+        config, service = config_and_service
2600
+        cli._save_config(config)
2601
+        config['services'].pop(service, None)
2623 2602
         _result = self.runner.invoke(
2624 2603
             cli.derivepassphrase_vault,
2625 2604
             ['--delete', '--', service],
... ...
@@ -2628,10 +2607,19 @@ class ConfigMergingStateMachine(stateful.RuleBasedStateMachine):
2628 2607
         )
2629 2608
         result = tests.ReadableResult.parse(_result)
2630 2609
         assert result.clean_exit(empty_stderr=False)
2610
+        assert cli._load_config() == config
2611
+        return config
2631 2612
 
2632
-    @stateful.rule()
2633
-    def purge_all(self) -> None:
2634
-        self.current_config = {'services': {}}
2613
+    @stateful.rule(
2614
+        target=configuration,
2615
+        config=configuration.filter(lambda c: 0 < len(c['services']) < 5),
2616
+    )
2617
+    def purge_all(
2618
+        self,
2619
+        config: _types.VaultConfig,
2620
+    ) -> _types.VaultConfig:
2621
+        cli._save_config(config)
2622
+        config = {'services': {}}
2635 2623
         _result = self.runner.invoke(
2636 2624
             cli.derivepassphrase_vault,
2637 2625
             ['--clear'],
... ...
@@ -2640,35 +2628,43 @@ class ConfigMergingStateMachine(stateful.RuleBasedStateMachine):
2640 2628
         )
2641 2629
         result = tests.ReadableResult.parse(_result)
2642 2630
         assert result.clean_exit(empty_stderr=False)
2631
+        assert cli._load_config() == config
2632
+        return config
2643 2633
 
2644 2634
     @stateful.rule(
2645
-        config=stateful.consumes(configurations),
2635
+        target=configuration,
2636
+        base_config=configuration,
2637
+        config_to_import=configuration,
2646 2638
         overwrite=strategies.booleans(),
2647 2639
     )
2648
-    def import_configuraton(
2640
+    def import_configuration(
2649 2641
         self,
2650
-        config: _types.VaultConfig,
2642
+        base_config: _types.VaultConfig,
2643
+        config_to_import: _types.VaultConfig,
2651 2644
         overwrite: bool,
2652
-    ) -> None:
2653
-        self.current_config = (
2654
-            self.fold_configs(config, self.current_config)
2645
+    ) -> _types.VaultConfig:
2646
+        cli._save_config(base_config)
2647
+        config = (
2648
+            self.fold_configs(config_to_import, base_config)
2655 2649
             if not overwrite
2656
-            else config
2650
+            else config_to_import
2657 2651
         )
2658
-        assert _types.is_vault_config(self.current_config)
2652
+        assert _types.is_vault_config(config)
2659 2653
         _result = self.runner.invoke(
2660 2654
             cli.derivepassphrase_vault,
2661 2655
             ['--import', '-']
2662 2656
             + (['--overwrite-existing'] if overwrite else []),
2663
-            input=json.dumps(config),
2657
+            input=json.dumps(config_to_import),
2664 2658
             catch_exceptions=False,
2665 2659
         )
2666 2660
         assert tests.ReadableResult.parse(_result).clean_exit(
2667 2661
             empty_stderr=False
2668 2662
         )
2663
+        assert cli._load_config() == config
2664
+        return config
2669 2665
 
2670 2666
     def teardown(self) -> None:
2671 2667
         self.exit_stack.close()
2672 2668
 
2673 2669
 
2674
-TestConfigMerging = ConfigMergingStateMachine.TestCase
2670
+TestConfigManagement = ConfigManagementStateMachine.TestCase
2675 2671