8000 fix(minimal): bound the workspace-upload unpack-error buffer by norrietaylor · Pull Request #928 · gominimal/minimal · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions crates/minimal/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,10 +223,14 @@ impl Client {
crate::file_upload::stream_tar_zstd(dir, channel.make_writer()).await?;

// Wait for the channel to close to signal that unpacking is done.
// The daemon relays any unpack failure on extended-data stream 1 and
// only then closes, so accumulate that stream under the same cap the
// diagnostic download uses — a daemon spraying extended data must not
// be able to balloon the client's memory.
let mut err = Vec::new();
while let Some(msg) = channel.wait().await {
if let russh::ChannelMsg::ExtendedData { data, ext: 1 } = msg {
err.extend_from_slice(&data);
append_daemon_error(&mut err, &data);
}
}
if !err.is_empty() {
Expand Down Expand Up @@ -359,8 +363,9 @@ impl Client {
}
}

/// Cap on the accumulated daemon error message (extended-data stream 1) for a
/// diagnostic-bundle download.
/// Cap on the accumulated daemon error message (extended-data stream 1) a
/// streaming RPC will buffer — shared by the diagnostic-bundle download and
/// the workspace-file upload.
const DAEMON_ERROR_MAX: usize = 64 * 1024;

/// How long [`Client::download_diag_bundle`] keeps reading after the size cap
Expand Down Expand Up @@ -426,6 +431,19 @@ mod tests {
use super::{Client, resolve_socket_path};
use std::path::Path;

/// The daemon-error accumulator is bounded: a daemon that sprays extended
/// data on stream 1 (during a diagnostic download or a workspace upload)
/// must not be able to grow the client's buffer past the cap.
#[test]
fn append_daemon_error_is_capped() {
let mut buf = Vec::new();
let chunk = vec![b'x'; 8 * 1024];
for _ in 0..64 {
super::append_daemon_error(&mut buf, &chunk);
}
assert_eq!(buf.len(), super::DAEMON_ERROR_MAX);
}

#[test]
fn socket_path_honors_override() {
let sock = resolve_socket_path(Some(Path::new("/tmp/minimal-test"))).unwrap();
Expand Down
24 changes: 24 additions & 0 deletions crates/minimal/tests/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,30 @@ async fn activate_uploads_project_files() {
assert!(mfile.starts_with(b"# test"));
}

/// A workspace upload whose unpack fails on the daemon must surface as an
/// `Err`, not a silent success. The daemon relays the failure on
/// extended-data stream 1 and only then closes the channel; the client reads
/// to that close, so the failure cannot be swallowed. Regression test for the
/// silent-upload-failure half of #824.
#[tokio::test]
async fn upload_workspace_files_surfaces_daemon_unpack_error() {
let (_daemon, args) = setup().await;
let mut client = connect_daemon(&args).await.unwrap();

// A well-formed but unknown session id: the daemon can't resolve a
8000 // destination for the upload, reports the failure, and closes.
let project = tempfile::TempDir::new().unwrap();
std::fs::write(project.path().join("hello.txt"), "hello world").unwrap();

let result = client
.upload_workspace_files(SessionId::nil(), project.path())
.await;
assert!(
result.is_err(),
"upload to an unknown session must fail, not report success: {result:?}"
);
Comment on lines +314 to +320
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the daemon-side error, not merely any error.

This can pass if upload preparation or channel I/O fails before the daemon reports its unpack error. Assert the daemon failed to unpack project files prefix emitted by crates/minimal/src/client.rs.

Suggested assertion
-    assert!(
-        result.is_err(),
-        "upload to an unknown session must fail, not report success: {result:?}"
-    );
+    let err = result.expect_err("upload to an unknown session must fail");
+    assert!(
+        err.to_string()
+            .contains("daemon failed to unpack project files"),
+        "expected daemon unpack failure, got: {err:#}"
+    );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let result = client
.upload_workspace_files(SessionId::nil(), project.path())
.await;
assert!(
result.is_err(),
"upload to an unknown session must fail, not report success: {result:?}"
);
let err = result.expect_err("upload to an unknown session must fail");
assert!(
err.to_string()
.contains("daemon failed to unpack project files"),
"expected daemon unpack failure, got: {err:#}"
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/minimal/tests/cli.rs` around lines 314 - 320, Update the test around
upload_workspace_files to assert that the returned error contains the
daemon-side “daemon failed to unpack project files” prefix from the client error
path, rather than only checking result.is_err(). Preserve the unknown-session
scenario and ensure unrelated preparation or channel I/O errors do not satisfy
the assertion.

}

// --- attach (smart resolution) ---

/// `min attach` with no session argument and `--no-input` errors cleanly when
Expand Down
0