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
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();