#![allow(missing_docs)]
#[path = "support/patch_env.rs"]
mod patch_env;
use patch_env::{API_TOKEN, PatchServer, assert_error_code, etag, mcp_call_tool, mcp_request};
use reqwest::StatusCode;
const NORMAL_MAX_PATCHABLE_SIZE: u64 = 10 * 1024 * 1024;
#[tokio::test]
#[ignore = "requires SeaweedFS + Qdrant testcontainers"]
async fn bytes_mode_patch_replaces_requested_byte_span() {
let server = PatchServer::start(NORMAL_MAX_PATCHABLE_SIZE).await;
let original = "0123456789".repeat(10);
let first_etag = server.put_text("bytes.md", &original).await;
let response = server
.patch_content_range("bytes.md", "bytes 0-9/*", Some(&first_etag), "ABCDEFGHIJ")
.await;
assert_eq!(response.status(), StatusCode::OK);
let patched_etag = etag(&response);
assert_ne!(patched_etag, first_etag);
assert_eq!(
server.get_text("bytes.md").await,
format!("ABCDEFGHIJ{}", &original[10..])
);
}
#[tokio::test]
#[ignore = "requires SeaweedFS + Qdrant testcontainers"]
async fn lines_mode_patch_replaces_inclusive_line_span() {
let server = PatchServer::start(NORMAL_MAX_PATCHABLE_SIZE).await;
let first_etag = server
.put_text("lines.md", "one\ntwo\nthree\nfour\nfive\n")
.await;
let response = server
.patch_content_range("lines.md", "lines 2-3/*", Some(&first_etag), "TWO\nTHREE\n")
.await;
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
server.get_text("lines.md").await,
"one\nTWO\nTHREE\nfour\nfive\n"
);
}
#[tokio::test]
#[ignore = "requires SeaweedFS + Qdrant testcontainers"]
async fn append_with_explicit_if_match_grows_object() {
let server = PatchServer::start(NORMAL_MAX_PATCHABLE_SIZE).await;
let head_etag = server.put_text("append-if-match.md", "alpha\n").await;
let response = server
.patch_append("append-if-match.md", Some(&head_etag), "beta\n")
.await;
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(server.get_text("append-if-match.md").await, "alpha\nbeta\n");
}
#[tokio::test]
#[ignore = "requires SeaweedFS + Qdrant testcontainers"]
async fn append_without_if_match_grows_object_via_http() {
let server = PatchServer::start(NORMAL_MAX_PATCHABLE_SIZE).await;
server.put_text("append-no-if-match.md", "alpha\n").await;
let response = server
.patch_append("append-no-if-match.md", None, "beta\n")
.await;
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
server.get_text("append-no-if-match.md").await,
"alpha\nbeta\n"
);
}
#[tokio::test]
#[ignore = "requires SeaweedFS + Qdrant testcontainers"]
async fn mcp_append_without_if_match_grows_object() {
let server = PatchServer::start(NORMAL_MAX_PATCHABLE_SIZE).await;
server.put_text("mcp-append.md", "alpha\n").await;
let initialize = mcp_request(
&server.client,
&server.mcp_url,
0,
"initialize",
serde_json::json!({
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": { "name": "notedthat-patch-e2e", "version": "0" }
}),
)
.await;
assert!(
initialize.get("result").is_some(),
"initialize failed: {initialize}"
);
let tool_response = mcp_call_tool(
&server.client,
&server.mcp_url,
1,
"append",
serde_json::json!({
"kb": server.kb,
"path": "mcp-append.md",
"content": "beta\n"
}),
)
.await;
assert!(
tool_response.get("result").is_some(),
"append failed: {tool_response}"
);
assert_eq!(server.get_text("mcp-append.md").await, "alpha\nbeta\n");
}
#[tokio::test]
#[ignore = "requires SeaweedFS + Qdrant testcontainers"]
async fn sequential_patch_uses_advanced_etag_for_second_write() {
let server = PatchServer::start(NORMAL_MAX_PATCHABLE_SIZE).await;
let first_etag = server.put_text("sequential.md", "one\ntwo\nthree\n").await;
let first_response = server
.patch_content_range("sequential.md", "lines 1-1/*", Some(&first_etag), "ONE\n")
.await;
assert_eq!(first_response.status(), StatusCode::OK);
let second_etag = etag(&first_response);
let second_response = server
.patch_content_range("sequential.md", "lines 2-2/*", Some(&second_etag), "TWO\n")
.await;
assert_eq!(second_response.status(), StatusCode::OK);
assert_ne!(etag(&second_response), second_etag);
assert_eq!(server.get_text("sequential.md").await, "ONE\nTWO\nthree\n");
}
#[tokio::test]
#[ignore = "requires SeaweedFS + Qdrant testcontainers"]
async fn concurrent_patches_with_same_if_match_surface_conflicts() {
let server = PatchServer::start(NORMAL_MAX_PATCHABLE_SIZE).await;
let starting_etag = server.put_text("conflict.md", "one\ntwo\nthree\n").await;
let url = server.object_url("conflict.md");
let mut handles = Vec::new();
for payload in ["winner-a\n", "winner-b\n", "winner-c\n"] {
let client = server.client.clone();
let url = url.clone();
let etag = starting_etag.clone();
handles.push(tokio::spawn(async move {
client
.patch(url)
.header("Authorization", format!("Bearer {API_TOKEN}"))
.header("Content-Range", "lines 1-1/*")
.header("If-Match", etag)
.body(payload.to_owned())
.send()
.await
.expect("concurrent PATCH should return")
.status()
}));
}
let mut statuses = Vec::new();
for handle in handles {
statuses.push(handle.await.expect("PATCH task should join"));
}
let successes = statuses
.iter()
.filter(|status| **status == StatusCode::OK)
.count();
let conflicts = statuses
.iter()
.filter(|status| **status == StatusCode::PRECONDITION_FAILED)
.count();
assert_eq!(
successes, 1,
"exactly one PATCH should succeed: {statuses:?}"
);
assert!(
conflicts >= 2,
"at least two PATCHes should fail with 412: {statuses:?}"
);
}
#[tokio::test]
#[ignore = "requires SeaweedFS + Qdrant testcontainers"]
async fn patch_rejects_object_larger_than_max_patchable_size_before_splice() {
let server = PatchServer::start(10).await;
server.put_text("pre-cap.md", "already too large").await;
let response = server.patch_append("pre-cap.md", None, "!").await;
assert_error_code(response, StatusCode::PAYLOAD_TOO_LARGE, "payload_too_large").await;
}
#[tokio::test]
#[ignore = "requires SeaweedFS + Qdrant testcontainers"]
async fn patch_rejects_result_larger_than_max_patchable_size_after_splice() {
let server = PatchServer::start(20).await;
server.put_text("post-cap.md", "123456789012345").await;
let response = server.patch_append("post-cap.md", None, "ABCDEFGHIJ").await;
assert_error_code(response, StatusCode::PAYLOAD_TOO_LARGE, "payload_too_large").await;
}
#[tokio::test]
#[ignore = "requires SeaweedFS + Qdrant testcontainers"]
async fn bytes_patch_without_if_match_returns_invalid_request() {
let server = PatchServer::start(NORMAL_MAX_PATCHABLE_SIZE).await;
server.put_text("missing-if-match.md", "0123456789").await;
let response = server
.patch_content_range("missing-if-match.md", "bytes 0-1/*", None, "AB")
.await;
assert_error_code(response, StatusCode::BAD_REQUEST, "invalid_request").await;
}
#[tokio::test]
#[ignore = "requires SeaweedFS + Qdrant testcontainers"]
async fn if_match_star_on_patch_returns_invalid_request() {
let server = PatchServer::start(NORMAL_MAX_PATCHABLE_SIZE).await;
server.put_text("star.md", "0123456789").await;
let response = server
.patch_content_range("star.md", "bytes 0-1/*", Some("*"), "AB")
.await;
assert_error_code(response, StatusCode::BAD_REQUEST, "invalid_request").await;
}
#[tokio::test]
#[ignore = "requires SeaweedFS + Qdrant testcontainers"]
async fn line_patch_out_of_range_returns_dual_416_headers_and_empty_body() {
let server = PatchServer::start(NORMAL_MAX_PATCHABLE_SIZE).await;
let body = "one\ntwo\nthree\nfour\nfive\n";
let first_etag = server.put_text("out-of-range.md", body).await;
let response = server
.patch_content_range(
"out-of-range.md",
"lines 100-200/*",
Some(&first_etag),
"replacement\n",
)
.await;
assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE);
assert_eq!(response.headers()["content-range"], "lines */5");
assert_eq!(
response.headers()["x-content-range-bytes"],
format!("*/{}", body.len())
);
let error_body = response.text().await.expect("416 body should read");
assert!(
error_body.is_empty(),
"416 body should be empty, got {error_body:?}"
);
}
#[tokio::test]
#[ignore = "requires SeaweedFS + Qdrant testcontainers"]
async fn patch_persists_body_when_indexer_accepts_event() {
let server = PatchServer::start(NORMAL_MAX_PATCHABLE_SIZE).await;
let first_etag = server.put_text("ghost-state.md", "alpha\n").await;
let response = server
.patch_append("ghost-state.md", Some(&first_etag), "beta\n")
.await;
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(server.get_text("ghost-state.md").await, "alpha\nbeta\n");
}