{"record":{"id":"29803edcc41ccf40","repo":"jdx/mise","slug":"must-not-record-a-pin-before-replacement-succeeds","errorCode":null,"errorMessage":"must not record a pin before replacement succeeds","messagePattern":"must not record a pin before replacement succeeds","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/plugins/packslip.rs","lineNumber":345,"sourceCode":"        assert!(result.is_err());\n        assert_eq!(\n            std::fs::read_to_string(plugin.join(\"metadata.lua\")).unwrap(),\n            \"previous\"\n        );\n    }\n\n    #[test]\n    fn failed_replacement_restores_previous_plugin() {\n        let temp = tempfile::tempdir().unwrap();\n        let plugin = temp.path().join(\"plugin\");\n        std::fs::create_dir(&plugin).unwrap();\n        std::fs::write(plugin.join(\"metadata.lua\"), \"previous\").unwrap();\n        assert!(\n            replace(\n                &temp.path().join(\"missing\"),\n                &plugin,\n                &temp.path().join(\"backup\"),\n                || panic!(\"must not record a pin before replacement succeeds\")\n            )\n            .is_err()\n        );\n        assert_eq!(\n            std::fs::read_to_string(plugin.join(\"metadata.lua\")).unwrap(),\n            \"previous\"\n        );\n    }\n}\n","sourceCodeStart":327,"sourceCodeEnd":355,"githubUrl":"https://github.com/jdx/mise/blob/afd2eddd3a50c16190efc1c7e94404b48f72af57/src/plugins/packslip.rs#L327-L355","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before (regression in replace())\nlet pin_result = record_pin();\nfs::rename(&payload, &plugin)?;\n// after\nfs::rename(&payload, &plugin)?;\nlet pin_result = record_pin();","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"// Rust: the panic is a test assertion — no catch needed. If embedding replace(), assert the Err path:\nlet result = replace(&payload, &plugin, &backup, || record_pin());\nassert!(result.is_err());\n// and verify the previous plugin content is untouched.","preventionTips":["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"],"tags":["rust","test","packslip","atomicity"],"backgroundTag":"internal-invariant-violation","analyzedSha":"afd2eddd3a50c16190efc1c7e94404b48f72af57","analyzedAt":"2026-09-09T01:38:25.179Z","contentChangedAt":"2026-09-09T01:38:25.179Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}