diff --git a/src/bonsai/bonsai/bim/handler.py b/src/bonsai/bonsai/bim/handler.py
index a7fc1dff67..c8f91ed6ee 100644
--- a/src/bonsai/bonsai/bim/handler.py
+++ b/src/bonsai/bonsai/bim/handler.py
@@ -320,9 +320,11 @@ def loadIfcStore(scene: bpy.types.Scene) -> None:
IfcStore.purge()
refresh_ui_data()
if not tool.Ifc.get():
+ tool.Autosave.cancel_timer()
return
tool.Ifc.schema()
IfcStore.relink_all_objects()
+ tool.Autosave.reset_timer()
@persistent
diff --git a/src/bonsai/bonsai/bim/module/project/__init__.py b/src/bonsai/bonsai/bim/module/project/__init__.py
index 945cac5f66..caa9458471 100644
--- a/src/bonsai/bonsai/bim/module/project/__init__.py
+++ b/src/bonsai/bonsai/bim/module/project/__init__.py
@@ -18,6 +18,7 @@
import bpy
+import bonsai.tool as tool
from . import decorator, gizmo, operator, prop, ui, workspace
classes = (
@@ -58,6 +59,9 @@ classes = (
operator.LinkIfc,
operator.LoadBlendMetadataAndIFC,
operator.LoadLink,
+ operator.AutosavePrompt,
+ operator.LoadAutosavedRecoveryPopup,
+ operator.LoadAutosavedRecovery,
operator.LoadLinkedProject,
operator.LoadProject,
operator.LoadProjectElements,
@@ -136,6 +140,7 @@ def register():
def unregister():
if not bpy.app.background:
bpy.utils.unregister_tool(workspace.ExploreTool)
+ tool.Autosave.cancel_timer()
del bpy.types.Scene.BIMProjectProperties
del bpy.types.Scene.MeasureToolSettings
bpy.app.handlers.load_post.remove(decorator.toggle_decorations_on_load)
diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py
index b0d9fc166d..d289df44c0 100644
--- a/src/bonsai/bonsai/bim/module/project/operator.py
+++ b/src/bonsai/bonsai/bim/module/project/operator.py
@@ -985,8 +985,10 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
),
default=False,
)
+ skip_autosave_recovery: bpy.props.BoolProperty(default=False, options={"HIDDEN", "SKIP_SAVE"})
use_detailed_tooltip: bpy.props.BoolProperty(default=False, options={"HIDDEN"})
filename_ext = ".ifc"
+ skip_recent: bpy.props.BoolProperty(default=False, options={"HIDDEN", "SKIP_SAVE"})
if TYPE_CHECKING:
filepath: str
@@ -995,6 +997,7 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
use_relative_path: bool
should_start_fresh_session: bool
import_without_ifc_data: bool
+ skip_autosave_recovery: bool
use_detailed_tooltip: bool
@classmethod
@@ -1041,7 +1044,26 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
return tooltip
+ def check_autosave_recovery(self, context: bpy.types.Context) -> set["rna_enums.OperatorReturnItems"] | None:
+ if self.skip_autosave_recovery:
+ return None
+ autosaved_filepath = tool.Autosave.get_newer_autosaved_path(self.get_filepath_abs())
+ if not autosaved_filepath:
+ return None
+ return bpy.ops.bim.load_autosaved_recovery_popup(
+ "INVOKE_DEFAULT",
+ original_filepath=str(self.get_filepath_abs()),
+ autosaved_filepath=autosaved_filepath,
+ is_advanced=self.is_advanced,
+ use_relative_path=self.use_relative_path,
+ should_start_fresh_session=self.should_start_fresh_session,
+ import_without_ifc_data=self.import_without_ifc_data,
+ )
+
def execute(self, context):
+ if recovery := self.check_autosave_recovery(context):
+ return recovery
+
if (
tool.Blender.get_addon_preferences().save_metadata_blend_file
and self.should_start_fresh_session
@@ -1136,7 +1158,8 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
props.should_save_metadata_for_this_file = metadata_doc is not None
tool.Blender.register_toolbar()
- tool.Project.add_recent_ifc_project(self.get_filepath_abs())
+ if not self.skip_recent:
+ tool.Project.add_recent_ifc_project(self.get_filepath_abs())
if self.is_advanced:
pass
@@ -1149,10 +1172,13 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
except:
bonsai.last_error = traceback.format_exc()
raise
+ tool.Autosave.reset_timer()
return {"FINISHED"}
def invoke(self, context, event):
if self.filepath:
+ if recovery := self.check_autosave_recovery(context):
+ return recovery
return self.execute(context)
return ImportHelper.invoke(self, context, event)
@@ -1947,6 +1973,7 @@ class ExportIFC(bpy.types.Operator, ExportHelper):
json_compact: bpy.props.BoolProperty(name="Export Compact IFCJSON", default=False)
should_save_as: bpy.props.BoolProperty(name="Should Save As", default=False, options={"HIDDEN"})
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False)
+ skip_recent: bpy.props.BoolProperty(default=False, options={"HIDDEN", "SKIP_SAVE"})
if TYPE_CHECKING:
filter_glob: str
@@ -2007,6 +2034,18 @@ class ExportIFC(bpy.types.Operator, ExportHelper):
return {"FINISHED"}
def _execute(self, context):
+ project_props = tool.Project.get_project_props()
+ project_props.use_relative_project_path = self.use_relative_path
+
+ # Fallback if filepath is not set
+ if not getattr(self, "filepath", None) or self.filepath.strip() in ("", ".ifc"):
+ props = tool.Blender.get_bim_props()
+ if props.ifc_file:
+ self.filepath = str(tool.Blender.ensure_blender_path_is_abs(Path(props.ifc_file)))
+ else:
+ self.report({"ERROR"}, "No filepath available for saving.")
+ return {"CANCELLED"}
+
committed, failed_commits = tool.Parametric.commit_pending_edits()
# Previews are session-transient — discard rather than commit. Sibling
# gizmo polls gate on each preview's is_active flag, and a stuck flag
@@ -2069,7 +2108,8 @@ class ExportIFC(bpy.types.Operator, ExportHelper):
settings.logger.info("Export finished in {:.2f} seconds".format(time.time() - start))
print("Export finished in {:.2f} seconds".format(time.time() - start))
# New project created in Bonsai should be in recent projects too.
- tool.Project.add_recent_ifc_project(Path(output_file))
+ if not self.skip_recent:
+ tool.Project.add_recent_ifc_project(Path(output_file))
props = tool.Project.get_project_props()
if props.use_relative_project_path and bpy.data.is_saved:
output_file = os.path.relpath(output_file, bpy.path.abspath("//"))
@@ -2103,6 +2143,7 @@ class ExportIFC(bpy.types.Operator, ExportHelper):
)
bonsai.bim.handler.refresh_ui_data()
+ tool.Autosave.reset_timer()
@classmethod
def description(cls, context, properties):
@@ -2111,6 +2152,127 @@ class ExportIFC(bpy.types.Operator, ExportHelper):
return "Save the IFC file. Will save both .IFC/.BLEND files if synced together"
+class LoadAutosavedRecoveryPopup(bpy.types.Operator):
+ bl_idname = "bim.load_autosaved_recovery_popup"
+ bl_label = "Recover Autosaved File"
+ bl_options = {"REGISTER", "UNDO"}
+
+ original_filepath: bpy.props.StringProperty(options={"SKIP_SAVE"})
+ autosaved_filepath: bpy.props.StringProperty(options={"SKIP_SAVE"})
+ is_advanced: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
+ use_relative_path: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
+ should_start_fresh_session: bpy.props.BoolProperty(default=True, options={"SKIP_SAVE"})
+ import_without_ifc_data: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
+
+ def draw(self, context):
+ layout = self.layout
+ layout.label(text="A newer autosaved copy was found:", icon="INFO")
+ layout.label(text=os.path.basename(self.autosaved_filepath))
+ layout.separator()
+ layout.label(text=f"Original: {os.path.basename(self.original_filepath)}")
+ layout.label(text="Which one do you want to load?")
+
+ row = layout.row(align=True)
+ op = row.operator("bim.load_autosaved_recovery", text="Load Original", icon="LOOP_BACK")
+ op.file_type = "ORIGINAL"
+ self._pass_props(op)
+
+ op = row.operator("bim.load_autosaved_recovery", text="Load Autosave", icon="LOOP_FORWARDS")
+ op.file_type = "AUTOSAVE"
+ self._pass_props(op)
+
+ def _pass_props(self, op):
+ op.original_filepath = self.original_filepath
+ op.autosaved_filepath = self.autosaved_filepath
+ op.is_advanced = self.is_advanced
+ op.use_relative_path = self.use_relative_path
+ op.should_start_fresh_session = self.should_start_fresh_session
+ op.import_without_ifc_data = self.import_without_ifc_data
+
+ def invoke(self, context, event):
+ return context.window_manager.invoke_popup(self, width=420)
+
+ def execute(self, context):
+ # This should almost never run
+ self.report({"INFO"}, "Popup closed without choosing")
+ return {"FINISHED"}
+
+
+class LoadAutosavedRecovery(bpy.types.Operator):
+ bl_idname = "bim.load_autosaved_recovery"
+ bl_label = "Recover Autosaved File"
+ bl_options = {"REGISTER", "UNDO"}
+
+ file_type: bpy.props.StringProperty(default="AUTOSAVE")
+ original_filepath: bpy.props.StringProperty(options={"SKIP_SAVE"})
+ autosaved_filepath: bpy.props.StringProperty(options={"SKIP_SAVE"})
+ is_advanced: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
+ use_relative_path: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
+ should_start_fresh_session: bpy.props.BoolProperty(default=True, options={"SKIP_SAVE"})
+ import_without_ifc_data: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
+
+ def execute(self, context):
+ if self.file_type == "ORIGINAL":
+ filepath = self.original_filepath
+ else:
+ filepath = self.autosaved_filepath
+
+ # Call the real loader
+ result = bpy.ops.bim.load_project(
+ filepath=filepath,
+ skip_autosave_recovery=True, # Prevent infinite loop
+ is_advanced=self.is_advanced,
+ use_relative_path=self.use_relative_path,
+ should_start_fresh_session=self.should_start_fresh_session,
+ import_without_ifc_data=self.import_without_ifc_data,
+ skip_recent=(self.file_type == "AUTOSAVE")
+ )
+
+ # If user chose autosave, override the stored path
+ if self.file_type == "AUTOSAVE":
+ tool.Ifc.set_path(self.original_filepath)
+
+ return result
+
+
+class AutosavePrompt(bpy.types.Operator):
+ bl_idname = "bim.autosave_prompt"
+ bl_label = "Autosave Reminder"
+ bl_options = set()
+
+ def invoke(self, context, event):
+ return context.window_manager.invoke_props_dialog(
+ self, width=400, confirm_text="Save", title="Autosave Reminder"
+ )
+
+ def draw(self, context):
+ layout = self.layout
+ layout.label(text="The autosave timer has expired.", icon="INFO")
+ layout.label(text="Would you like to save your IFC project now?")
+
+ def execute(self, context):
+ # Get current IFC path
+ props = tool.Blender.get_bim_props()
+ current_ifc_path = props.ifc_file
+
+ if not current_ifc_path:
+ self.report({"WARNING"}, "No IFC file path set. Please save manually.")
+ tool.Autosave.reset_timer()
+ return {"CANCELLED"}
+
+ # Call save_project with explicit filepath using EXEC_DEFAULT
+ result = bpy.ops.bim.save_project(
+ "EXEC_DEFAULT", filepath=current_ifc_path, should_save_as=False, skip_recent=True
+ )
+
+ tool.Autosave.reset_timer()
+ return result
+
+ def cancel(self, context):
+ tool.Autosave.reset_timer()
+ return {"CANCELLED"}
+
+
class LoadLinkedProject(bpy.types.Operator, ImportHelper):
bl_idname = "bim.load_linked_project"
bl_label = "Load Project For Viewing Only"
diff --git a/src/bonsai/bonsai/bim/ui.py b/src/bonsai/bonsai/bim/ui.py
index 2c2886f464..42092ff558 100644
--- a/src/bonsai/bonsai/bim/ui.py
+++ b/src/bonsai/bonsai/bim/ui.py
@@ -577,6 +577,43 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
should_disable_undo_on_save: BoolProperty(
name="Disable Undo When Saving (Faster saves, no undo for you!)", default=False
)
+
+ def update_autosave_settings(self, context: bpy.types.Context) -> None:
+ if self.autosave_enabled:
+ tool.Autosave.reset_timer()
+ else:
+ tool.Autosave.cancel_timer()
+
+ autosave_enabled: BoolProperty(
+ name="Enable IFC Autosave Timer",
+ description="Periodically remind you to save or automatically create a backup copy of the IFC file",
+ default=False,
+ update=update_autosave_settings,
+ )
+ autosave_interval_minutes: bpy.props.IntProperty(
+ name="Autosave Interval (Minutes)",
+ description="Time between autosave reminders or backups. The timer resets whenever you open or save a project",
+ default=10,
+ min=1,
+ max=1440,
+ update=update_autosave_settings,
+ )
+ autosave_mode: bpy.props.EnumProperty(
+ name="Autosave Mode",
+ items=[
+ (
+ "PROMPT",
+ "Prompt to Save",
+ "Show a dialog offering to save the IFC project when the timer expires",
+ ),
+ (
+ "BACKUP",
+ "Automatic Backup",
+ "Save a backup copy as filename_autosaved.ifc when the timer expires",
+ ),
+ ],
+ default="PROMPT",
+ )
should_stream: BoolProperty(name="Stream Data From IFC-SPF (Only for advanced users)", default=False)
should_always_cache: BoolProperty(
name="Always Cache Geometry",
@@ -689,6 +726,9 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
bsdd_load_test_dictionaries: bool
bsdd_baseurl: str
should_disable_undo_on_save: bool
+ autosave_enabled: bool
+ autosave_interval_minutes: int
+ autosave_mode: Literal["PROMPT", "BACKUP"]
should_stream: bool
should_always_cache: bool
occurrence_name_style: Literal["CLASS", "TYPE", "CUSTOM"]
@@ -837,6 +877,12 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
def draw_other_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
layout.prop(self, "opening_focus_opacity")
layout.prop(self, "should_disable_undo_on_save")
+ layout.separator()
+ layout.label(text="Autosave:")
+ layout.prop(self, "autosave_enabled")
+ if self.autosave_enabled:
+ layout.prop(self, "autosave_interval_minutes")
+ layout.prop(self, "autosave_mode")
layout.prop(self, "should_stream")
layout.prop(self, "should_always_cache")
layout.label(text="bSDD:")
diff --git a/src/bonsai/bonsai/tool/__init__.py b/src/bonsai/bonsai/tool/__init__.py
index f927e5e1e1..817780ddb2 100644
--- a/src/bonsai/bonsai/tool/__init__.py
+++ b/src/bonsai/bonsai/tool/__init__.py
@@ -80,3 +80,6 @@ from bonsai.tool.type import Type
from bonsai.tool.unit import Unit
from bonsai.tool.wall import Wall
from bonsai.tool.web import Web
+
+# Have to move after import of tool.drawing
+from bonsai.tool.autosave import Autosave
diff --git a/src/bonsai/bonsai/tool/autosave.py b/src/bonsai/bonsai/tool/autosave.py
new file mode 100644
index 0000000000..7fb4a04e33
--- /dev/null
+++ b/src/bonsai/bonsai/tool/autosave.py
@@ -0,0 +1,149 @@
+# Bonsai - OpenBIM Blender Add-on
+# Copyright (C) 2026
+#
+# This file is part of Bonsai.
+#
+# Bonsai is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Bonsai is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Bonsai. If not, see .
+#
+# This file was generated with the assistance of an AI coding tool.
+
+from __future__ import annotations
+
+import logging
+import os
+from pathlib import Path
+from typing import Callable, Union
+
+import bpy
+
+import bonsai.tool as tool
+from bonsai.bim import export_ifc
+from bonsai.bim.module.model import preview_base
+
+
+AUTOSAVING_SUFFIX = "_autosaving.ifc"
+AUTOSAVED_SUFFIX = "_autosaved.ifc"
+
+_timer_callback: Union[Callable[[], None], None] = None
+
+
+class Autosave:
+ @classmethod
+ def get_paths(cls, ifc_path: Union[str, Path]) -> tuple[Path, Path, Path]:
+ path = Path(ifc_path)
+ stem = path.stem if path.suffix.lower() == ".ifc" else path.name
+ parent = path.parent
+ main_path = path if path.suffix.lower() == ".ifc" else parent / f"{stem}.ifc"
+ autosaving_path = parent / f"{stem}{AUTOSAVING_SUFFIX}"
+ autosaved_path = parent / f"{stem}{AUTOSAVED_SUFFIX}"
+ return main_path, autosaving_path, autosaved_path
+
+ @classmethod
+ def get_active_ifc_path(cls) -> Union[Path, None]:
+ props = tool.Blender.get_bim_props()
+ if not props.ifc_file:
+ return None
+ path = tool.Blender.ensure_blender_path_is_abs(Path(props.ifc_file))
+ if path.suffix.lower() != ".ifc":
+ return None
+ return path
+
+ @classmethod
+ def is_enabled(cls) -> bool:
+ return bool(tool.Blender.get_addon_preferences().autosave_enabled)
+
+ @classmethod
+ def get_interval_seconds(cls) -> float:
+ minutes = tool.Blender.get_addon_preferences().autosave_interval_minutes
+ return max(1.0, float(minutes) * 60.0)
+
+ @classmethod
+ def is_eligible(cls) -> bool:
+ return cls.is_enabled() and tool.Ifc.get() is not None and cls.get_active_ifc_path() is not None
+
+ @classmethod
+ def cancel_timer(cls) -> None:
+ global _timer_callback
+ if _timer_callback is not None and bpy.app.timers.is_registered(_timer_callback):
+ bpy.app.timers.unregister(_timer_callback)
+ _timer_callback = None
+
+ @classmethod
+ def reset_timer(cls) -> None:
+ cls.cancel_timer()
+ if not cls.is_eligible():
+ return
+
+ def on_timer() -> None:
+ cls._on_timer_expired()
+ return None
+
+ global _timer_callback
+ _timer_callback = on_timer
+ bpy.app.timers.register(on_timer, first_interval=cls.get_interval_seconds())
+
+ @classmethod
+ def _on_timer_expired(cls) -> None:
+ if not cls.is_eligible():
+ return
+
+ prefs = tool.Blender.get_addon_preferences()
+ bim_props = tool.Blender.get_bim_props()
+
+ if bim_props.is_dirty:
+ if prefs.autosave_mode == "PROMPT":
+ bpy.ops.bim.autosave_prompt("INVOKE_DEFAULT")
+ elif prefs.autosave_mode == "BACKUP":
+ try:
+ cls.perform_backup(bpy.context)
+ except Exception as error:
+ print(f"Bonsai: autosave backup failed: {error}")
+ cls.reset_timer()
+
+ @classmethod
+ def perform_backup(cls, context: bpy.types.Context) -> None:
+ ifc_path = cls.get_active_ifc_path()
+ if ifc_path is None:
+ return
+
+ _, autosaving_path, autosaved_path = cls.get_paths(ifc_path)
+ autosaving_path.parent.mkdir(parents=True, exist_ok=True)
+
+ tool.Parametric.commit_pending_edits()
+ preview_base.discard_pending_previews(context.scene)
+
+ logger = logging.getLogger("ExportIFC")
+ output_file = autosaving_path.as_posix().replace("\\", "/")
+ settings = export_ifc.IfcExportSettings.factory(context, output_file, logger)
+ export_ifc.IfcExporter(settings).export()
+
+ try:
+ os.replace(autosaving_path, autosaved_path)
+ except OSError:
+ if autosaving_path.is_file():
+ autosaving_path.unlink(missing_ok=True)
+ raise
+
+ @classmethod
+ def get_newer_autosaved_path(cls, ifc_path: Union[str, Path]) -> Union[str, None]:
+ path = Path(ifc_path)
+ if path.suffix.lower() != ".ifc" or not path.is_file():
+ return None
+
+ _, _, autosaved_path = cls.get_paths(path)
+ if not autosaved_path.is_file():
+ return None
+ if autosaved_path.stat().st_mtime > path.stat().st_mtime:
+ return autosaved_path.as_posix().replace("\\", "/")
+ return None
diff --git a/src/bonsai/test/bim/module/project/test_autosave.py b/src/bonsai/test/bim/module/project/test_autosave.py
new file mode 100644
index 0000000000..8f1b5277a5
--- /dev/null
+++ b/src/bonsai/test/bim/module/project/test_autosave.py
@@ -0,0 +1,68 @@
+# Bonsai - OpenBIM Blender Add-on
+# Copyright (C) 2026
+#
+# This file is part of Bonsai.
+#
+# Bonsai is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Bonsai is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Bonsai. If not, see .
+#
+# This file was generated with the assistance of an AI coding tool.
+
+import os
+import time
+from pathlib import Path
+
+import pytest
+
+from bonsai.tool.autosave import AUTOSAVED_SUFFIX, AUTOSAVING_SUFFIX, Autosave
+
+pytestmark = pytest.mark.project
+
+
+class TestAutosavePaths:
+ def test_get_paths_for_ifc_file(self):
+ main_path, autosaving_path, autosaved_path = Autosave.get_paths("/tmp/myfile.ifc")
+ assert main_path == Path("/tmp/myfile.ifc")
+ assert autosaving_path == Path(f"/tmp/myfile{AUTOSAVING_SUFFIX}")
+ assert autosaved_path == Path(f"/tmp/myfile{AUTOSAVED_SUFFIX}")
+
+ def test_get_newer_autosaved_path_when_missing(self, tmp_path):
+ ifc_path = tmp_path / "myfile.ifc"
+ ifc_path.write_text("ifc")
+ assert Autosave.get_newer_autosaved_path(ifc_path) is None
+
+ def test_get_newer_autosaved_path_when_older(self, tmp_path):
+ ifc_path = tmp_path / "myfile.ifc"
+ autosaved_path = tmp_path / f"myfile{AUTOSAVED_SUFFIX}"
+ ifc_path.write_text("ifc")
+ autosaved_path.write_text("autosaved")
+ past = time.time() - 10
+ os.utime(ifc_path, (past, past))
+ os.utime(autosaved_path, (time.time(), time.time()))
+ assert Autosave.get_newer_autosaved_path(ifc_path) == autosaved_path.as_posix()
+
+ def test_get_newer_autosaved_path_when_not_newer(self, tmp_path):
+ ifc_path = tmp_path / "myfile.ifc"
+ autosaved_path = tmp_path / f"myfile{AUTOSAVED_SUFFIX}"
+ ifc_path.write_text("ifc")
+ autosaved_path.write_text("autosaved")
+ now = time.time()
+ os.utime(ifc_path, (now, now))
+ past = now - 10
+ os.utime(autosaved_path, (past, past))
+ assert Autosave.get_newer_autosaved_path(ifc_path) is None
+
+ def test_get_newer_autosaved_path_ignores_non_ifc(self, tmp_path):
+ path = tmp_path / "myfile.ifczip"
+ path.write_text("zip")
+ assert Autosave.get_newer_autosaved_path(path) is None