test: add serve-all integration tests (push, fs-sync, mixed protocol)

Three new subprocess tests exercise the serve-all dispatch layer:
- serve_all_push_revisions: onion revisions dispatched correctly
- serve_all_fs_sync_cold_copy: FsDirManifest dispatch path
- serve_all_mixed_protocol_push_then_fs_sync: both protocols on same
  server without interference

Also adds ServeAllServer harness and imports Path in subprocess.rs.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
osobh
2026-04-05 22:53:52 -05:00
co-authored by Claude Sonnet 4.6
parent 50001b64ac
commit dcd2106da4
+207 -2
View File
@@ -16,7 +16,7 @@
//! synchronisation point before issuing client commands.
use std::io::{BufRead, BufReader};
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use clawhdf5_onion::writer::OnionFile;
@@ -683,7 +683,7 @@ fn ssh_push_cold_copy() {
let fake_ssh = tmp.path().join("fake-ssh");
write_fake_ssh_onion(&fake_ssh);
let (src_dir, src_h5, _) = make_versioned_h5(10);
let (_src_dir, src_h5, _) = make_versioned_h5(10);
let dst_dir = TempDir::new().unwrap();
let dst_h5 = dst_dir.path().join("data.h5");
std::fs::write(&dst_h5, H5_MAGIC).unwrap();
@@ -750,3 +750,208 @@ fn ssh_pull_cold_copy() {
// Suppress unused-variable warnings.
drop(dst_dir);
}
// ─────────────────────────────────────────────────────────────────────────────
// serve-all integration tests
//
// serve-all listens on one port and dispatches incoming connections to the
// correct handler based on the first SyncMessage variant received:
// - IbltRequest / ManifestRequest → onion revision handler
// - FsDirManifest → FS general-file handler
// - Hdf5ManifestRequest → HDF5 dataset handler
//
// These tests verify that all three dispatch paths work correctly.
// ─────────────────────────────────────────────────────────────────────────────
/// A running `clawsync serve-all` process.
struct ServeAllServer {
child: std::process::Child,
/// Bound address ("127.0.0.1:PORT").
pub addr: String,
}
impl ServeAllServer {
/// Start `clawsync serve-all <dir> --bind 127.0.0.1:0`.
fn start(dir: &Path) -> Self {
let mut child = Command::new(BIN)
.args([
"serve-all",
dir.to_str().unwrap(),
"--bind",
"127.0.0.1:0",
])
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.expect("failed to spawn clawsync serve-all");
let stdout = child.stdout.take().unwrap();
let mut reader = BufReader::new(stdout);
let mut line = String::new();
reader
.read_line(&mut line)
.expect("serve-all did not print a startup line");
// "ClawSync server listening on 127.0.0.1:PORT"
let addr = line
.trim()
.strip_prefix("ClawSync server listening on ")
.unwrap_or_else(|| panic!("unexpected serve-all startup line: {line:?}"))
.to_string();
// Drain stdout so the server doesn't get SIGPIPE.
std::thread::spawn(move || {
let mut buf = String::new();
while reader.read_line(&mut buf).unwrap_or(0) > 0 {
buf.clear();
}
});
Self { child, addr }
}
}
impl Drop for ServeAllServer {
fn drop(&mut self) {
let _ = self.child.kill();
let _ = self.child.wait();
}
}
/// `serve-all` dispatch: onion push → push 5 revisions to a serve-all instance
/// (via the ManifestRequest / IbltRequest dispatch path).
#[test]
fn serve_all_push_revisions() {
let root = TempDir::new().unwrap();
let server = ServeAllServer::start(root.path());
let (src_dir, src_h5, _) = make_versioned_h5(5);
let agent_path = format!("{}/agent.claws", server.addr);
let out = Command::new(BIN)
.args(["push", src_h5.to_str().unwrap(), &agent_path])
.output()
.expect("failed to spawn clawsync push");
assert!(
out.status.success(),
"serve-all push failed:\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
let count = parse_push_revision_count(&String::from_utf8_lossy(&out.stdout));
assert_eq!(count, Some(5), "expected 5 revisions pushed via serve-all");
// Verify the onion file was written under the serve-all root.
let written = await_revisions(&root.path().join("agent.claws"), 5);
assert_eq!(written, 5, "serve-all did not persist 5 revisions");
drop(src_dir);
}
/// `serve-all` dispatch: FS sync → sync a directory tree via the FsDirManifest
/// dispatch path.
#[test]
fn serve_all_fs_sync_cold_copy() {
use std::fs;
let root = TempDir::new().unwrap();
let server = ServeAllServer::start(root.path());
// Build a small source tree.
let src = TempDir::new().unwrap();
fs::write(src.path().join("alpha.bin"), vec![0xA0u8; 4096]).unwrap();
fs::write(src.path().join("beta.bin"), vec![0xB0u8; 8192]).unwrap();
let sub = src.path().join("sub");
fs::create_dir(&sub).unwrap();
fs::write(sub.join("gamma.bin"), vec![0xC0u8; 2048]).unwrap();
// FsSyncServer always writes relative paths into its serve_root; the path
// component of the remote URL is not forwarded — files land directly in root.
let remote = format!("{}/", server.addr);
let out = Command::new(BIN)
.args(["sync", src.path().to_str().unwrap(), &remote])
.output()
.expect("failed to spawn clawsync sync");
assert!(
out.status.success(),
"serve-all fs sync failed:\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("3 added"),
"expected 3 added files; got: {stdout}"
);
// Verify files landed directly under root (serve_root = serve-all root).
let check = |rel: &str| {
let p = root.path().join(rel);
assert!(p.exists(), "missing: {rel}");
};
check("alpha.bin");
check("beta.bin");
check("sub/gamma.bin");
}
/// `serve-all` dispatch: multiple protocol types on the same server — push
/// revisions then FS-sync files, verifying that both dispatch paths co-exist
/// without interfering.
#[test]
fn serve_all_mixed_protocol_push_then_fs_sync() {
use std::fs;
let root = TempDir::new().unwrap();
let server = ServeAllServer::start(root.path());
// 1) Push 3 onion revisions.
let (_src_dir, src_h5, _) = make_versioned_h5(3);
let agent_path = format!("{}/memory.claws", server.addr);
let push_out = Command::new(BIN)
.args(["push", src_h5.to_str().unwrap(), &agent_path])
.output()
.expect("failed to spawn clawsync push");
assert!(
push_out.status.success(),
"push failed:\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&push_out.stdout),
String::from_utf8_lossy(&push_out.stderr),
);
let push_count = parse_push_revision_count(&String::from_utf8_lossy(&push_out.stdout));
assert_eq!(push_count, Some(3));
// 2) FS-sync 2 files (separate sub-directory so paths don't collide).
let src_fs = TempDir::new().unwrap();
fs::write(src_fs.path().join("x.bin"), vec![0xAAu8; 1024]).unwrap();
fs::write(src_fs.path().join("y.bin"), vec![0xBBu8; 2048]).unwrap();
// Path component is ignored by FsSyncServer; sync to root directly.
let remote_fs = format!("{}/", server.addr);
let sync_out = Command::new(BIN)
.args(["sync", src_fs.path().to_str().unwrap(), &remote_fs])
.output()
.expect("failed to spawn clawsync sync");
assert!(
sync_out.status.success(),
"fs sync failed:\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&sync_out.stdout),
String::from_utf8_lossy(&sync_out.stderr),
);
let sync_stdout = String::from_utf8_lossy(&sync_out.stdout);
assert!(
sync_stdout.contains("2 added"),
"expected 2 added; got: {sync_stdout}"
);
// Verify onion revisions survived the second connection.
let written = await_revisions(&root.path().join("memory.claws"), 3);
assert_eq!(written, 3, "onion revisions lost after FS sync on same server");
}