diff --git a/fnd/config.py b/fnd/config.py index bf5f61dc..44494796 100644 --- a/fnd/config.py +++ b/fnd/config.py @@ -1053,9 +1053,10 @@ def load(path: Path | None = None) -> Config: def ensure_current(config_path: Path | None = None) -> list[str]: """Rewrite the config in the current shape, keeping a timestamped backup. - Returns what was applied, empty when the file was already current. Called - once at startup so every config converges on one syntax rather than each - legacy shape being honoured forever. + Returns what was applied, empty when the file already holds the current + values: builds that word a hint differently must not rewrite each other's + file. Called once at startup so every config converges on one syntax rather + than each legacy shape being honoured forever. """ from fnd._perms import secure_write_text from fnd.config_migrations import migrate @@ -1069,7 +1070,7 @@ def ensure_current(config_path: Path | None = None) -> list[str]: version, applied = migrate(raw) config = Config.model_validate(raw) rendered = render_config(config, preserved=extract_preserved(text), version=version) - if rendered == text: + if rendered == text or tomllib.loads(rendered) == tomllib.loads(text): return [] _refuse_lossy(rendered, config, raw) stamp = dt.datetime.now().strftime("%Y%m%d-%H%M%S") diff --git a/tests/test_config_render.py b/tests/test_config_render.py index 8332e33b..a3b4effe 100644 --- a/tests/test_config_render.py +++ b/tests/test_config_render.py @@ -617,6 +617,26 @@ def test_ensure_current_is_a_no_op_on_a_current_file(self, tmp_path: Path) -> No assert conf.ensure_current(path) == [] assert not list(tmp_path.glob("config.toml.bak-*")) + def test_reworded_generated_comments_alone_are_not_rewritten(self, tmp_path: Path) -> None: + """Builds that word a hint differently otherwise rewrite each other's file on launch.""" + path = tmp_path / "config.toml" + current = render_config(_sample()) + older = current.replace("# fnd configuration.", "# fnd configuration, older wording.") + assert older != current + path.write_text(older, encoding="utf-8") + assert conf.ensure_current(path) == [] + assert path.read_text(encoding="utf-8") == older + assert not list(tmp_path.glob("config.toml.bak-*")) + + def test_a_value_in_a_non_canonical_form_is_still_rewritten(self, tmp_path: Path) -> None: + path = tmp_path / "config.toml" + current = render_config(_sample()) + assert '"~/Notes"' in current + absolute = f"'{Path.home() / 'Notes'}'" # a literal string, so Windows backslashes parse + path.write_text(current.replace('"~/Notes"', absolute), encoding="utf-8") + assert conf.ensure_current(path) == ["Adopt the canonical layout"] + assert path.read_text(encoding="utf-8") == current + class TestWritersStayCanonical: @pytest.mark.parametrize( diff --git a/tests/test_match_nav_integration.py b/tests/test_match_nav_integration.py index 6e608632..1d00a38e 100644 --- a/tests/test_match_nav_integration.py +++ b/tests/test_match_nav_integration.py @@ -599,8 +599,10 @@ async def test_stops_hold_their_rows_while_a_scroll_awaits_its_render( message=f"the stops never read as settled at y={start}", ) if state == "visible_map": - # The render Textual makes after a scroll: visible widgets only. - app.screen._refresh_layout(scroll=True) + # Textual's post-scroll fast path, called directly: the screen takes + # it only while no widget has a layout pending, which a slow runner + # does not guarantee. + app.screen._compositor.reflow_visible(app.screen, app.screen.outer_size) assert app.screen._compositor._visible_map is not None pane.scroll_to(y=end, animate=False, immediate=True)