Implement closed typed effect path

This commit is contained in:
tmk241
2026-09-01 00:58:29 +02:00
parent 353174604e
commit 31a0f02211
58 changed files with 4247 additions and 5933 deletions
+5 -38
View File
@@ -19,7 +19,7 @@ use std::sync::atomic::{AtomicUsize, Ordering};
use tower::ServiceExt;
static MUTATION_CALLS: AtomicUsize = AtomicUsize::new(0);
static BOUNDARY_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
static BOUNDARY_TEST_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
async fn bounded_mutation(_: InteractionRequest) -> StatusCode {
MUTATION_CALLS.fetch_add(1, Ordering::SeqCst);
@@ -40,10 +40,7 @@ async fn multipart_mutation(request: InteractionRequest) -> StatusCode {
#[tokio::test]
async fn interaction_boundary_honors_media_type_and_host_body_limit() {
// test req: security/003
let _guard = BOUNDARY_TEST_LOCK
.lock()
.unwrap_or_else(|error| error.into_inner());
let _guard = BOUNDARY_TEST_LOCK.lock().await;
MUTATION_CALLS.store(0, Ordering::SeqCst);
let app = Router::new()
.route("/mutate", post(bounded_mutation))
@@ -104,9 +101,7 @@ async fn interaction_boundary_honors_media_type_and_host_body_limit() {
#[tokio::test]
async fn interaction_boundary_extracts_multipart_fields_and_files() {
let _guard = BOUNDARY_TEST_LOCK
.lock()
.unwrap_or_else(|error| error.into_inner());
let _guard = BOUNDARY_TEST_LOCK.lock().await;
let boundary = "hemx-boundary";
let body = concat!(
"--hemx-boundary\r\n",
@@ -136,14 +131,11 @@ async fn interaction_boundary_extracts_multipart_fields_and_files() {
.unwrap();
assert_eq!(response.status(), StatusCode::NO_CONTENT);
// test req: multipart/001 req: multipart/002 req: multipart/003
}
#[tokio::test]
async fn interaction_boundary_skips_unnamed_parts_and_rejects_invalid_multipart() {
let _guard = BOUNDARY_TEST_LOCK
.lock()
.unwrap_or_else(|error| error.into_inner());
let _guard = BOUNDARY_TEST_LOCK.lock().await;
let app = Router::new().route("/mutate", post(bounded_mutation));
let missing_boundary = app
.clone()
@@ -208,7 +200,6 @@ async fn interaction_boundary_skips_unnamed_parts_and_rejects_invalid_multipart(
.await
.unwrap();
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
// test req: multipart/001 req: multipart/003
}
fn selector(value: &str) -> Selector {
@@ -266,12 +257,10 @@ fn page_mode_detects_only_explicit_partial_header_values() {
headers.insert(HEMX_PARTIAL_HEADER, value.parse().unwrap());
assert_eq!(PageMode::from_headers(&headers), PageMode::Partial);
}
// test req: page_swap/001
}
#[test]
fn page_request_wraps_full_pages_and_leaves_partials_unwrapped() {
// req: test/005
assert!(!PageRequest {
mode: PageMode::Full
}
@@ -321,7 +310,6 @@ fn page_request_wraps_full_pages_and_leaves_partials_unwrapped() {
#[test]
fn page_request_wraps_safe_html_full_pages_and_leaves_partials_unwrapped() {
// req: axum_integration/001 req: html_safety/001 req: html_safety/002 req: test/005
let full = PageRequest {
mode: PageMode::Full,
}
@@ -389,7 +377,6 @@ fn page_response_constructors_preserve_mode_and_optional_fingerprint() {
partial.fingerprint(fingerprint).fingerprint,
Some(fingerprint)
);
// test req: page_swap/001 req: abi/005
}
#[tokio::test]
@@ -410,7 +397,6 @@ async fn full_page_response_preserves_html_and_fingerprint_without_partial_heade
to_bytes(response.into_body(), 1024).await.unwrap().as_ref(),
b"<html>Full</html>"
);
// test req: page_swap/001 req: abi/005
}
#[test]
@@ -447,7 +433,6 @@ fn interaction_form_parses_handle_and_fields() {
#[test]
fn interaction_form_parses_typed_values() {
// req: form/004 req: dx/003
let form =
InteractionForm::parse_urlencoded(b"__h=42&count=7&bad=nope").expect("form should parse");
@@ -470,7 +455,6 @@ fn interaction_form_requires_numeric_handle() {
#[test]
fn interaction_form_preserves_hidden_csrf_fields_for_extractors() {
// req: auth/004
let form = InteractionForm::parse_urlencoded(b"__h=42&csrf_token=abc123&title=Hello").unwrap();
assert_eq!(form.handle_id, 42);
@@ -480,7 +464,6 @@ fn interaction_form_preserves_hidden_csrf_fields_for_extractors() {
#[test]
fn interaction_request_dispatches_with_concise_handlers_helper() {
// req: axum_integration/003 req: ceremony/001 req: dx/003
let request = InteractionRequest::from(InteractionForm::for_handle(
Handle::<()>::new(7),
Vec::new(),
@@ -498,7 +481,6 @@ fn interaction_request_dispatches_with_concise_handlers_helper() {
#[test]
fn state_interactions_starts_stateful_wiring_without_nested_closures() {
// req: axum_integration/003 req: ceremony/001 req: dx/003
fn ping(prefix: String) -> impl IntoEffect {
Slot::<String>::new(3).text(format!("{prefix}: ping"))
}
@@ -568,7 +550,6 @@ fn interaction_form_accessors_preserve_repeated_values_and_decode_diagnostics()
.fields()
.iter()
.any(|pair| pair == &("count".into(), "7".into())));
// test req: form/002 req: form/004 req: multipart/001
}
#[tokio::test]
@@ -607,12 +588,10 @@ async fn interaction_form_rejections_return_stable_status_and_diagnostic() {
message.as_bytes()
);
}
// test req: failure/003 req: multipart/003
}
#[test]
fn interaction_request_dispatches_typed_form_inputs() {
// req: axum_integration/003 req: form/004 req: canonical_authoring/003
let request = InteractionRequest::from(InteractionForm::for_handle(
Handle::<()>::new(8),
vec![("project_id".to_owned(), "42".to_owned())],
@@ -632,7 +611,6 @@ fn interaction_request_dispatches_typed_form_inputs() {
#[test]
fn interaction_request_rejects_invalid_typed_form_inputs() {
// req: axum_integration/003 req: form/004 req: canonical_authoring/004
let request = InteractionRequest::from(InteractionForm::for_handle(
Handle::<()>::new(8),
vec![("project_id".to_owned(), "nope".to_owned())],
@@ -654,7 +632,6 @@ fn interaction_request_rejects_invalid_typed_form_inputs() {
#[test]
fn interaction_request_dispatches_typed_state_handlers() {
// req: axum_integration/003 req: form/004 req: canonical_authoring/003
fn open_project(multiplier: u32, input: OpenProject) -> impl IntoEffect {
Slot::<String>::new(3).text(input.project_id.0 * multiplier)
}
@@ -702,7 +679,6 @@ impl IntoHandlerFailure for UiFailure {
#[tokio::test]
async fn interaction_request_dispatches_async_typed_state_extractors() {
// req: axum_integration/003 req: form/004 req: canonical_authoring/003
async fn open_project(
State(multiplier): State<u32>,
Form(input): Form<OpenProject>,
@@ -729,7 +705,6 @@ async fn interaction_request_dispatches_async_typed_state_extractors() {
#[tokio::test]
async fn interaction_request_reports_async_result_handler_errors() {
// req: axum_integration/003 req: form/004 req: failure/003
async fn open_project(
State(_multiplier): State<u32>,
Form(_input): Form<OpenProject>,
@@ -762,7 +737,6 @@ async fn interaction_request_reports_async_result_handler_errors() {
#[tokio::test]
async fn interaction_request_maps_async_result_errors_to_effects() {
// req: axum_integration/003 req: failure/004
async fn open_project(
State(_multiplier): State<u32>,
Form(_input): Form<OpenProject>,
@@ -792,7 +766,6 @@ async fn interaction_request_maps_async_result_errors_to_effects() {
#[test]
fn interaction_request_maps_sync_result_errors_to_effects() {
// req: axum_integration/003 req: failure/004
fn open_project(
State(_multiplier): State<u32>,
Form(_input): Form<OpenProject>,
@@ -821,7 +794,6 @@ fn interaction_request_maps_sync_result_errors_to_effects() {
#[test]
fn interactions_dispatch_by_checked_handle() {
// req: ceremony/004 req: public_api/001
let title = Slot::<String>::new(7);
let create = Handle::<()>::new(42);
let request = InteractionRequest::from(InteractionForm::for_handle(
@@ -862,7 +834,6 @@ async fn registry_contains_registered_sync_and_async_handles_and_async_falls_bac
sync_response.batch.ops,
vec![Slot::<String>::new(1).text("sync")]
);
// test req: axum_integration/003 req: public_api/001
}
#[tokio::test]
@@ -883,9 +854,8 @@ async fn state_result_registration_paths_preserve_state_form_and_failures() {
typed,
String::from("typed"),
|state: State<String>, Form(form): Form<OpenProject>| {
Err::<Effect, _>(HandlerBoom).map_err(|error| {
Err::<Effect, _>(HandlerBoom).inspect_err(|_| {
let _ = (state, form);
error
})
},
)
@@ -1011,7 +981,6 @@ async fn state_result_registration_paths_preserve_state_form_and_failures() {
response.batch.ops,
vec![Slot::<String>::new(1).text("typed-async:9")]
);
// test req: axum_integration/003 req: derive_handler/005
}
#[test]
@@ -1074,7 +1043,6 @@ async fn effect_and_dispatch_responses_preserve_status_wire_and_diagnostics() {
let response = DispatchRejection::HandlerError(HandlerFailure::Effects(batch)).into_response();
assert_eq!(response.headers()[header::CONTENT_TYPE], HEMX_CONTENT_TYPE);
// test req: axum_integration/003 req: failure/005 req: wire/007
}
#[tokio::test]
@@ -1106,7 +1074,6 @@ async fn runtime_js_response_serves_embedded_runtime() {
);
}
// req: axum_integration/005
#[test]
fn runtime_js_path_is_content_hashed() {
let path = runtime_js_path();