use axum::body::Body;
use axum::http::{Request, StatusCode};
use http_body_util::BodyExt;
use ocserve_http::{AppState, LlmRegistry, Payloads, Wires};
use serde_json::{Value, json};
use std::sync::Arc;
use tower::ServiceExt;
fn git(dir: &std::path::Path, args: &[&str]) {
let o = std::process::Command::new("git")
.current_dir(dir)
.args(args)
.output()
.unwrap();
assert!(
o.status.success(),
"git {:?}: {}",
args,
String::from_utf8_lossy(&o.stderr)
);
}
fn repo_fixture(tag: &str) -> (tempfile::TempDir, std::path::PathBuf) {
let tmp = tempfile::tempdir().unwrap();
let wt = tmp.path().join(tag);
std::fs::create_dir_all(&wt).unwrap();
git(&wt, &["init", "-q"]);
git(&wt, &["config", "user.email", "t@t"]);
git(&wt, &["config", "user.name", "t"]);
std::fs::write(wt.join("a.txt"), "one\n").unwrap();
git(&wt, &["add", "."]);
git(&wt, &["commit", "-qm", "init"]);
(tmp, wt)
}
fn app_for(wt: &std::path::Path, tag: &str) -> Arc<AppState> {
let data = std::env::temp_dir().join(format!("ocserve-vcsapply-{}-{tag}", std::process::id()));
let _ = std::fs::remove_dir_all(&data);
std::fs::create_dir_all(&data).unwrap();
let db = ocserve_store::writer::db_path(&data);
let writer = Arc::new(ocserve_store::Writer::spawn(db.clone()).unwrap());
let blobs = Arc::new(ocserve_store::BlobStore::new(data.join("blobs")).unwrap());
let st = AppState::with_wiring(
None,
Payloads::default(),
Wires {
db,
blobs,
writer,
llm: LlmRegistry::default(),
},
);
let _ = wt;
st
}
async fn post_to(
app: &axum::Router,
wt: &std::path::Path,
body: Value,
) -> axum::response::Response {
const ROUTE: &str = "/vcs/apply";
let uri = format!(
"{ROUTE}?directory={}",
urlencoding_simple(&wt.display().to_string())
);
post(app, &uri, body).await
}
fn urlencoding_simple(s: &str) -> String {
s.replace('%', "%25").replace(' ', "%20")
}
async fn post(app: &axum::Router, uri: &str, body: Value) -> axum::response::Response {
app.clone()
.oneshot(
Request::builder()
.method("POST")
.uri(uri)
.header("content-type", "application/json")
.body(Body::from(body.to_string()))
.unwrap(),
)
.await
.unwrap()
}
async fn json(resp: axum::response::Response) -> Value {
let b = resp.into_body().collect().await.unwrap().to_bytes();
serde_json::from_slice(&b).unwrap_or(Value::Null)
}
const GOOD_PATCH: &str = "--- a/a.txt\n+++ b/a.txt\n@@ -1 +1 @@\n-one\n+two\n";
#[tokio::test]
async fn apply_mutates_the_worktree_on_disk() {
let (_tmp, wt) = repo_fixture("good");
let st = app_for(&wt, "good");
let app = ocserve_http::router(st.clone());
let resp = post_to(&app, &wt, json!({"patch": GOOD_PATCH})).await;
assert_eq!(resp.status(), StatusCode::OK);
let v = json(resp).await;
assert_eq!(v["applied"], true, "success shape: {v}");
assert_eq!(
std::fs::read_to_string(wt.join("a.txt")).unwrap(),
"two\n",
"the patch must have been applied to the worktree"
);
}
#[tokio::test]
async fn conflicting_patch_leaves_the_worktree_unchanged() {
let (_tmp, wt) = repo_fixture("bad");
let st = app_for(&wt, "bad");
let app = ocserve_http::router(st.clone());
let bad = "--- a/a.txt\n+++ b/a.txt\n@@ -1 +1 @@\n-DOES NOT MATCH\n+nope\n";
let resp = post_to(&app, &wt, json!({"patch": bad})).await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let v = json(resp).await;
assert_eq!(v["name"], "VcsApplyError");
assert_eq!(v["data"]["reason"], "not-clean", "{v}");
assert_eq!(std::fs::read_to_string(wt.join("a.txt")).unwrap(), "one\n");
}
#[tokio::test]
async fn non_git_directory_reports_non_git() {
let tmp = tempfile::tempdir().unwrap();
let st = app_for(tmp.path(), "nongit");
let app = ocserve_http::router(st.clone());
let resp = post_to(&app, tmp.path(), json!({"patch": GOOD_PATCH})).await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let v = json(resp).await;
assert_eq!(v["data"]["reason"], "non-git", "{v}");
}
#[tokio::test]
async fn missing_patch_is_a_payload_error() {
let (_tmp, wt) = repo_fixture("missing");
let st = app_for(&wt, "missing");
let app = ocserve_http::router(st.clone());
let resp = post_to(&app, &wt, json!({})).await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let v = json(resp).await;
assert_eq!(v["data"]["kind"], "Payload", "{v}");
}
#[test]
fn pre_fix_shape_cannot_satisfy_the_side_effect_assertion() {
let pre_fix_status = StatusCode::BAD_REQUEST;
let pre_fix_body = json!({"name": "VcsApplyError", "data": {"message": "Patch can't be applied", "reason": "not-clean"}});
assert_ne!(
pre_fix_status,
StatusCode::OK,
"pre-fix never returned 200 for a valid patch"
);
assert_ne!(
pre_fix_body["applied"], true,
"pre-fix body cannot carry applied:true"
);
}