diff --git a/cli/src/fs.rs b/cli/src/fs.rs index 4d500ec..67a9db6 100644 --- a/cli/src/fs.rs +++ b/cli/src/fs.rs @@ -339,8 +339,12 @@ impl HostDescriptor for ActorFs<'_> { HostDescriptor::rename_at(&mut self.inner, fd, old_path, new_fd, new_path).await?; // Open writes and directories below a renamed folder move with it. if let Some((from, to)) = moved { + // `join("")` would add a trailing slash, so a renamed file itself + // needs its own case. let rebase = |p: &mut PathBuf| { - if let Ok(rest) = p.strip_prefix(&from) { + if *p == from { + *p = to.clone(); + } else if let Ok(rest) = p.strip_prefix(&from) { *p = to.join(rest); } }; diff --git a/cli/tests/host.rs b/cli/tests/host.rs index d89837f..6108bf0 100644 --- a/cli/tests/host.rs +++ b/cli/tests/host.rs @@ -776,6 +776,21 @@ async fn close_commits_open_file_after_parent_directory_rename() { host.stop().await; } +#[tokio::test] +async fn close_commits_a_staged_write_after_the_file_itself_is_renamed() { + let (_, host, mut mailbox) = filesystem_world("rename-staged-file").await; + let write = json!({"type": "write", "path": "/data/x", "text": "old"}); + ask_type(&host, &mut mailbox, "writer", write, "ok").await; + let open = json!({"type": "open-truncate", "path": "/data/x", "text": "new"}); + ask_type(&host, &mut mailbox, "writer", open, "opened").await; + let rename = json!({"type": "rename", "path": "/data/x", "to": "/data/x.renamed"}); + ask_type(&host, &mut mailbox, "writer", rename, "ok").await; + ask_type(&host, &mut mailbox, "writer", json!({"type": "close", "path": "/data/x"}), "closed").await; + let read = json!({"type": "read", "path": "/data/x.renamed"}); + assert_eq!(ask_type(&host, &mut mailbox, "writer", read, "content").await.body["text"], "new"); + host.stop().await; +} + #[tokio::test] async fn truncate_without_create_commits_existing_file_on_close() { let (_, host, mut mailbox) = filesystem_world("truncate-without-create").await;