From 98db679f6b5e0ef40ab46239d5fa49a347e9c19d Mon Sep 17 00:00:00 2001 From: vam Date: Tue, 3 Nov 2026 01:50:07 -0800 Subject: [PATCH] fix(sandbox): publish empty diff snapshots to clear stale views --- crates/sandbox-core/CHANGELOG.md | 6 +++ crates/sandbox-core/src/server.rs | 20 ++++---- crates/sandbox-local/tests/execute_command.rs | 47 +++++++++++++++++++ 3 files changed, 62 insertions(+), 11 deletions(-) diff --git a/crates/sandbox-core/CHANGELOG.md b/crates/sandbox-core/CHANGELOG.md index cde1057e..972fd6cd 100644 --- a/crates/sandbox-core/CHANGELOG.md +++ b/crates/sandbox-core/CHANGELOG.md @@ -1,3 +1,9 @@ +## Unreleased + +### Fixes + +- Send empty sandbox diff snapshots after the final change is removed, clearing stale diff views. + ## v0.1.0 (2026-09-03) diff --git a/crates/sandbox-core/src/server.rs b/crates/sandbox-core/src/server.rs index f57527d2..035d755a 100644 --- a/crates/sandbox-core/src/server.rs +++ b/crates/sandbox-core/src/server.rs @@ -1788,7 +1788,7 @@ async fn push_diff_view( }; let files: Vec = match &repo_state.mode { - SandboxMode::Direct => Vec::new(), + SandboxMode::Direct => return, _ => { let Ok(changed) = state.backend.diff_files(sandbox_dir, &repo_state).await else { return; @@ -1807,16 +1807,14 @@ async fn push_diff_view( } }; - if !files.is_empty() { - rap_protocol::send_view_update( - &state.callback_client, - &invocation.callback_url, - invocation.group_id.clone(), - "diff", - serde_json::json!({ "files": files }), - ) - .await; - } + rap_protocol::send_view_update( + &state.callback_client, + &invocation.callback_url, + invocation.group_id.clone(), + "diff", + serde_json::json!({ "files": files }), + ) + .await; } /// Build a pretty-printed unified diff for display in the CLI. diff --git a/crates/sandbox-local/tests/execute_command.rs b/crates/sandbox-local/tests/execute_command.rs index be8a924d..632656d8 100644 --- a/crates/sandbox-local/tests/execute_command.rs +++ b/crates/sandbox-local/tests/execute_command.rs @@ -234,3 +234,50 @@ async fn sandboxed_command_can_write_sccache_cache_dir() { std::env::remove_var("SCCACHE_DIR"); } } + +#[tokio::test] +async fn deleting_last_added_file_clears_diff_view() { + let repo = jj_init_with_file("README.md", "hello\n"); + let metadata = tempfile::tempdir().expect("create metadata directory"); + let server_url = start_test_server(metadata.path()).await; + let (callback_url, mut rx) = start_callback_channel() + .await + .expect("start callback channel"); + let group = "empty-diff-test"; + let result = invoke( + &server_url, + &callback_url, + group, + "clone_repo", + serde_json::json!({ "repo": repo.path().to_str().expect("repository path") }), + &mut rx, + None, + ) + .await; + assert!(result.contains("Repository initialized"), "{result}"); + + let (_, created_views) = invoke_collecting_views( + &server_url, + &callback_url, + group, + "create_file", + serde_json::json!({ "path": "added.txt", "content": "new content\n" }), + &mut rx, + 1, + ) + .await; + assert_eq!(created_views[0].content["files"][0]["path"], "added.txt"); + + let (_, deleted_views) = invoke_collecting_views( + &server_url, + &callback_url, + group, + "execute_command", + serde_json::json!({ "command": "rm added.txt" }), + &mut rx, + 1, + ) + .await; + assert_eq!(deleted_views[0].view_type, "diff"); + assert_eq!(deleted_views[0].content["files"], serde_json::json!([])); +}