diff --git a/src/slic3r/plugin/PluginFsUtils.cpp b/src/slic3r/plugin/PluginFsUtils.cpp index 7956d01bb0..4a5d16ff9c 100644 --- a/src/slic3r/plugin/PluginFsUtils.cpp +++ b/src/slic3r/plugin/PluginFsUtils.cpp @@ -754,7 +754,14 @@ bool read_install_state(const boost::filesystem::path& plugin_dir, PluginDescrip // truth for a cloud plugin's installed version: it records the version fetched from // the cloud at install time, independent of the (possibly stale) manifest/PEP723 // header that scan_directory parses into entry.version. - if (!state.installed_version.empty()) + // + // A local plugin has no cloud copy to diverge from: the entry file's own header is the only + // source of truth for its version. Trusting the sidecar there would pin the version shown + // in the UI to whatever it was at first install, even after the file is edited. + const bool is_local_install = state.installed_from == "local" && state.cloud_uuid.empty(); + if (is_local_install && !entry.version.empty()) + entry.installed_version = entry.version; + else if (!state.installed_version.empty()) entry.installed_version = state.installed_version; if (!state.cloud_uuid.empty()) entry.cloud = CloudPluginState{state.cloud_uuid, true, false, false}; diff --git a/tests/slic3rutils/test_plugin_install.cpp b/tests/slic3rutils/test_plugin_install.cpp index b860486979..0f3b9a7fcf 100644 --- a/tests/slic3rutils/test_plugin_install.cpp +++ b/tests/slic3rutils/test_plugin_install.cpp @@ -144,3 +144,43 @@ TEST_CASE("install-state sidecar is the source of truth for a cloud plugin's ins read_install_state(plugin_dir, scanned); CHECK(scanned.installed_version == "1.2.0"); } + +TEST_CASE("a local plugin's installed version follows its entry file header", "[PluginInstall]") +{ + ScopedDataDir data_dir_guard("local-installed-version"); + + const fs::path plugin_dir = data_dir_guard.dir / "plugin"; + fs::create_directories(plugin_dir); + + // Sidecar written when the local plugin was first installed at 1.0.0. + PluginInstallState state; + state.installed_from = "local"; + state.installed_version = "1.0.0"; + REQUIRE(write_install_state(plugin_dir, state)); + + // The user then edited the plugin file: the freshly scanned header says 1.1.0. + PluginDescriptor scanned; + scanned.version = "1.1.0"; + REQUIRE(read_install_state(plugin_dir, scanned)); + CHECK(scanned.installed_version == "1.1.0"); +} + +TEST_CASE("an unsubscribed cloud plugin follows its header version", "[PluginInstall]") +{ + ScopedDataDir data_dir_guard("unsubscribed-installed-version"); + + const fs::path plugin_dir = data_dir_guard.dir / "plugin"; + fs::create_directories(plugin_dir); + + // What keep_installed_plugin_as_local leaves behind: a normal local package. The sidecar still + // holds the version fetched from the cloud (1.2.0), but the header (1.0.0) is the source of truth. + PluginInstallState state; + state.installed_from = "local"; + state.installed_version = "1.2.0"; + REQUIRE(write_install_state(plugin_dir, state)); + + PluginDescriptor scanned; + scanned.version = "1.0.0"; + REQUIRE(read_install_state(plugin_dir, scanned)); + CHECK(scanned.installed_version == "1.0.0"); +}