jdx/mise · error
must not record a pin before replacement succeeds
Error message
must not record a pin before replacement succeeds
What it means
This panic comes from a unit test (failed_replacement_restores_previous_plugin) for packslip::replace, not from production code. The closure passed to replace() records the installed-version pin; the test passes a closure that panics to prove the invariant that the pin callback is never invoked when the payload replacement itself fails (here because the payload directory 'missing' does not exist). If you see this panic while running tests, the replace() implementation started recording a pin before the plugin directory swap had succeeded, which would leave a stale pin after a failed install.
Source
Thrown at src/plugins/packslip.rs:345
assert!(result.is_err());
assert_eq!(
std::fs::read_to_string(plugin.join("metadata.lua")).unwrap(),
"previous"
);
}
#[test]
fn failed_replacement_restores_previous_plugin() {
let temp = tempfile::tempdir().unwrap();
let plugin = temp.path().join("plugin");
std::fs::create_dir(&plugin).unwrap();
std::fs::write(plugin.join("metadata.lua"), "previous").unwrap();
assert!(
replace(
&temp.path().join("missing"),
&plugin,
&temp.path().join("backup"),
|| panic!("must not record a pin before replacement succeeds")
)
.is_err()
);
assert_eq!(
std::fs::read_to_string(plugin.join("metadata.lua")).unwrap(),
"previous"
);
}
}
View on GitHub (pinned to afd2eddd3a)
Solutions
- Keep the pin-recording closure invoked only after the payload replacement has fully succeeded and the old plugin has been moved to backup
- Ensure every early-return/`?` failure path in replace() (missing payload dir, copy error, backup error) bypasses the callback entirely
- Run `mise run test:unit` (or `cargo test --bin mise packslip`) to confirm the test passes after the refactor
Example fix
// before (regression in replace()) let pin_result = record_pin(); fs::rename(&payload, &plugin)?; // after fs::rename(&payload, &plugin)?; let pin_result = record_pin();
Defensive patterns
Strategy: try-catch
Try / catch
// Rust: the panic is a test assertion — no catch needed. If embedding replace(), assert the Err path: let result = replace(&payload, &plugin, &backup, || record_pin()); assert!(result.is_err()); // and verify the previous plugin content is untouched.
Prevention
- Never invoke side-effectful callbacks (pin writes) before the primary operation has succeeded
- Structure multi-step file operations as swap-then-commit so failure paths skip commit hooks
- Run the packslip unit tests after any change to replace() ordering
When it happens
Trigger: Running `cargo test failed_replacement_restores_previous_plugin` (or the packslip test module) against a modified replace() in src/plugins/packslip.rs that calls the pin-recording callback before the payload copy/rename completes, or calls it even when the payload path is missing and the operation returns Err.
Common situations: Refactoring packslip plugin replacement logic (reordering the backup/swap/pin steps, moving the pin write earlier, or changing error propagation so the callback runs on the failure path); a regression during work on the packslip backend's install/upgrade flow.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- the packslip gives no download URL for {}
- the packslip names an artifact {:?}, which is not a plain fi
- invalid remote action manifest ETag
- local CAS blob failed digest verification: {}
- bytes do not match the declared CAS digest
AI-assisted analysis of jdx/mise@afd2eddd3a (2026-09-09).
Data as JSON: /api/errors/29803edcc41ccf40.
Report an issue: GitHub.