fix(installtxn): do not read a target we could not probe as absent

recoverWorkspace read any os.Lstat failure on the target as nothing being
there, which enters the restore branch: the one branch that moves a tree,
taken on the strength of a question that never got an answer. Only a
not-exist error means the swap did not finish; every other probe failure
leaves the phase unknown, so it is reported and the workspace is left as it
was.

The same distinction the reconcilers already draw between a missing file and
a file that could not be read, applied to the probe that decides which branch
recovery takes at all.
This commit is contained in:
beardthelion committed 2026-09-10 11:31:37 -05:00
1 parent 66232eba89
commit 005fff72c3
2 files changed
+33

No files matched your search

+6
View File
@@ -211,6 +211,12 @@ func Recover(dir string, reconcile Reconciler) error {
// recoverWorkspace resolves one attributable workspace whose backup is present.
func recoverWorkspace(workspace string, target string, backup string, name string, reconcile Reconciler) error {
if _, err := os.Lstat(target); err != nil {
if !errors.Is(err, os.ErrNotExist) {
// A target we could not probe is not a target that is absent. Reading
// it as absent enters the restore branch, the one branch that moves a
// tree, on the strength of a question that never got an answer.
return fmt.Errorf("inspect install %s: %w", name, err)
}
// Nothing at the target, so the swap never finished and the backup is the
// only copy there is. There is no second tree to weigh it against, and
// asking could only produce an answer that throws it away.
+27
View File
@@ -953,3 +953,30 @@ func TestRecoverKeepsAttributionWhenRetirementFailsPartway(t *testing.T) {
t.Fatal("the second pass went silent on a workspace still holding a backup")
}
}
// A target we could not probe is not a target that is absent. Reading a failed
// Lstat as "nothing there" sends recovery into the restore branch, which is the
// one branch that moves a tree, on the strength of a question it never got an
// answer to. The recorded name here is too long for the filesystem to resolve,
// which is the one probe failure a test can produce without a fault seam.
func TestRecoverReportsATargetItCannotProbe(t *testing.T) {
dir := t.TempDir()
name := strings.Repeat("a", 300)
workspace := plantInterruptedCommit(t, dir, "demo", name, "old")
reconciler := &recordingReconciler{phase: PhaseCommitted}
err := Recover(dir, reconciler.reconcile)
if err == nil {
t.Fatal("a target that could not be probed must be reported")
}
if strings.Contains(err.Error(), "restore interrupted install") {
t.Fatalf("recovery tried to restore over a target it never resolved: %v", err)
}
if !strings.Contains(err.Error(), "inspect install") {
t.Fatalf("error does not report the probe failure: %v", err)
}
if _, statErr := os.Stat(filepath.Join(workspace, "previous")); statErr != nil {
t.Fatalf("the retained backup must stay put: %v", statErr)
}
}