Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions fnd/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")
Expand Down
20 changes: 20 additions & 0 deletions tests/test_config_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
6 changes: 4 additions & 2 deletions tests/test_match_nav_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading