Let custom Fennel assertions handle upgrades
A missing upgrade function falls back to install so existing scripts
keep working when their assertions report conflict-upgrade.

Assisted-by: GLM-5.3 via pi
change sxkwtuxptpxnpuslxlnsvyzponymyuos
commit d4a780560fe63cf8b1d07525c43cb5fc7bc96caf
author Alpha Chen <alpha@kejadlen.dev>
date
parent xyzlzyxz
diff --git a/.cargo/mutants.toml b/.cargo/mutants.toml
index 3b30e05..b36acaf 100644
--- a/.cargo/mutants.toml
+++ b/.cargo/mutants.toml
@@ -12,7 +12,6 @@ exclude_re = [
     "impl AssertionType for Brew<R>>::upgrade",
     "impl AssertionType for BrewBundle<R>>::upgrade",
     "impl AssertionType for Debug>::upgrade",
-    "impl AssertionType for LuaAssertion>::upgrade",
     "AssertionType::remove",
 
     # BrewBundle's status and install exist twice, split on
diff --git a/AGENTS.md b/AGENTS.md
index 1da2902..a5202df 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -137,7 +137,7 @@ The `Registry` struct in `registry.rs` dispatches assertion types. It checks Lua
 `runtime.rs` holds everything that touches Lua. `setup_lua` creates a Lua VM, loads the Fennel compiler, and registers a `frork` module with:
 
 - `frork.ok(type, ...)` — assert that a condition holds (dispatches through the registry)
-- `frork.register(name, {status=fn, install=fn})` — register a custom assertion type from Fennel
+- `frork.register(name, {status=fn, install=fn, upgrade=fn})` — register a custom assertion type from Fennel
 - `frork.utils` — utility functions exposed to Lua/Fennel scripts
 
 ### Utils module
@@ -156,7 +156,7 @@ On the Rust side, `ExpandedPath` handles `~` and `$ENV_VAR` expansion at the Lua
 
 ### Custom assertions
 
-Fennel scripts can register new assertion types via `frork.register(name, {status=fn, install=fn, display=fn})`. These become `LuaAssertionType` values dispatched through the same `AssertionType` trait. The optional `display` function controls how the assertion is printed.
+Fennel scripts can register new assertion types via `frork.register(name, {status=fn, install=fn, upgrade=fn, display=fn})`. These become `LuaAssertionType` values dispatched through the same `AssertionType` trait. The optional `upgrade` function handles a `conflict-upgrade` status when the user answers the upgrade prompt; when absent, upgrading falls back to `install`. The optional `display` function controls how the assertion is printed.
 
 ### frork-lua vs frork-cli
 
diff --git a/frork-cli/src/assertions.rs b/frork-cli/src/assertions.rs
index a131554..6689fbb 100644
--- a/frork-cli/src/assertions.rs
+++ b/frork-cli/src/assertions.rs
@@ -550,6 +550,7 @@ pub struct LuaAssertionType {
     pub display_fn: Option<LuaFunction>,
     pub status_fn: LuaFunction,
     pub install_fn: LuaFunction,
+    pub upgrade_fn: Option<LuaFunction>,
 }
 
 impl FromLua for LuaAssertionType {
@@ -559,11 +560,13 @@ impl FromLua for LuaAssertionType {
         let display_fn: Option<LuaFunction> = table.get("display").ok();
         let status_fn: LuaFunction = table.get("status")?;
         let install_fn: LuaFunction = table.get("install")?;
+        let upgrade_fn: Option<LuaFunction> = table.get("upgrade").ok();
 
         Ok(Self {
             display_fn,
             status_fn,
             install_fn,
+            upgrade_fn,
         })
     }
 }
@@ -629,11 +632,19 @@ impl AssertionType for LuaAssertion {
         Ok(())
     }
 
-    // cov-excl-start
     fn upgrade(&self) -> Result<()> {
-        todo!()
+        // A missing upgrade function falls back to install — re-running the
+        // install is the default way to bring a conflicted assertion in line.
+        if let Some(upgrade_fn) = self.assertion_type.upgrade_fn.as_ref() {
+            upgrade_fn
+                .call::<()>(self.args.clone())
+                .map_err(FrorkError::from)?;
+        } else {
+            self.install()?;
+        }
+        debug!("upgraded: {}", self);
+        Ok(())
     }
-    // cov-excl-stop
 }
 
 #[cfg(test)]
@@ -1231,6 +1242,7 @@ mod tests {
         )
         .unwrap();
         assert!(assertion_type.display_fn.is_none());
+        assert!(assertion_type.upgrade_fn.is_none());
 
         assert!(lua_assertion_type(&lua, r#"return {install = function() end}"#).is_err());
     }
@@ -1276,6 +1288,62 @@ mod tests {
         assert!(assertion.install().is_err());
     }
 
+    #[test]
+    fn test_lua_assertion_upgrade_fn_wins_over_install() {
+        let lua = Lua::new();
+        let assertion_type = lua_assertion_type(
+            &lua,
+            r#"return {
+                status = function() return "ok" end,
+                install = function(a) _G.ran = "install:" .. a end,
+                upgrade = function(a) _G.ran = "upgrade:" .. a end,
+            }"#,
+        )
+        .unwrap();
+
+        let assertion = LuaAssertion::new("custom", multi(&lua, r#"return "arg""#), assertion_type);
+        assertion.upgrade().unwrap();
+
+        let ran: String = lua.globals().get("ran").unwrap();
+        assert_eq!(ran, "upgrade:arg");
+    }
+
+    #[test]
+    fn test_lua_assertion_upgrade_falls_back_to_install() {
+        let lua = Lua::new();
+        let assertion_type = lua_assertion_type(
+            &lua,
+            r#"return {
+                status = function() return "ok" end,
+                install = function() _G.ran = "install" end,
+            }"#,
+        )
+        .unwrap();
+
+        let assertion = LuaAssertion::new("custom", LuaMultiValue::new(), assertion_type);
+        assertion.upgrade().unwrap();
+
+        let ran: String = lua.globals().get("ran").unwrap();
+        assert_eq!(ran, "install");
+    }
+
+    #[test]
+    fn test_lua_assertion_upgrade_propagates_failures() {
+        let lua = Lua::new();
+        let assertion_type = lua_assertion_type(
+            &lua,
+            r#"return {
+                status = function() return "ok" end,
+                install = function() end,
+                upgrade = function() error("upgrade boom") end,
+            }"#,
+        )
+        .unwrap();
+
+        let assertion = LuaAssertion::new("custom", LuaMultiValue::new(), assertion_type);
+        assert!(assertion.upgrade().is_err());
+    }
+
     #[test]
     fn test_lua_assertion_display_without_display_fn() {
         let lua = Lua::new();