atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
9374b3933189ddeefe1c4e1c10256e0d8285b2c7
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git

Blame: src/tests.rs

a6caa2f99786atagen2026-10-12 10:38 UTC1
use std::{sync::Arc, time::Duration};
a9b016e4b248atagen2026-10-10 08:12 UTC2
a9b016e4b248atagen2026-10-10 08:12 UTC3
use axum::{
a9b016e4b248atagen2026-10-10 08:12 UTC4
    Router,
a9b016e4b248atagen2026-10-10 08:12 UTC5
    body::{Body, to_bytes},
782069860236atagen2026-10-10 12:44 UTC6
    http::{Method, Request, StatusCode, Version, header},
a9b016e4b248atagen2026-10-10 08:12 UTC7
    response::Response,
a9b016e4b248atagen2026-10-10 08:12 UTC8
};
a9b016e4b248atagen2026-10-10 08:12 UTC9
use serde_json::Value;
a9b016e4b248atagen2026-10-10 08:12 UTC10
use tower::ServiceExt;
a9b016e4b248atagen2026-10-10 08:12 UTC11
782069860236atagen2026-10-10 12:44 UTC12
use crate::{
782069860236atagen2026-10-10 12:44 UTC13
    AppState, Config,
139164489032atagen2026-10-11 05:46 UTC14
    game::{
139164489032atagen2026-10-11 05:46 UTC15
        Floor, MumVisit, Run, Story,
9374b3933189atagen2026-10-12 11:15 UTC16
        balance::{BRANCH_TURN, SUSPICION_LIMIT},
139164489032atagen2026-10-11 05:46 UTC17
    },
782069860236atagen2026-10-10 12:44 UTC18
    router,
782069860236atagen2026-10-10 12:44 UTC19
};
782069860236atagen2026-10-10 12:44 UTC20
a9b016e4b248atagen2026-10-10 08:12 UTC21
async fn send(
a9b016e4b248atagen2026-10-10 08:12 UTC22
    app: &Router,
a9b016e4b248atagen2026-10-10 08:12 UTC23
    method: &str,
a9b016e4b248atagen2026-10-10 08:12 UTC24
    uri: &str,
a9b016e4b248atagen2026-10-10 08:12 UTC25
    headers: &[(&str, &str)],
a9b016e4b248atagen2026-10-10 08:12 UTC26
    body: &str,
a9b016e4b248atagen2026-10-10 08:12 UTC27
) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC28
    let mut request = Request::builder()
a9b016e4b248atagen2026-10-10 08:12 UTC29
        .method(Method::from_bytes(method.as_bytes()).unwrap())
a9b016e4b248atagen2026-10-10 08:12 UTC30
        .uri(uri);
a9b016e4b248atagen2026-10-10 08:12 UTC31
    for (name, value) in headers {
a9b016e4b248atagen2026-10-10 08:12 UTC32
        request = request.header(*name, *value);
a9b016e4b248atagen2026-10-10 08:12 UTC33
    }
a9b016e4b248atagen2026-10-10 08:12 UTC34
    app.clone()
a9b016e4b248atagen2026-10-10 08:12 UTC35
        .oneshot(request.body(Body::from(body.to_owned())).unwrap())
a9b016e4b248atagen2026-10-10 08:12 UTC36
        .await
a9b016e4b248atagen2026-10-10 08:12 UTC37
        .unwrap()
a9b016e4b248atagen2026-10-10 08:12 UTC38
}
a9b016e4b248atagen2026-10-10 08:12 UTC39
a9b016e4b248atagen2026-10-10 08:12 UTC40
async fn json(response: Response) -> Value {
a9b016e4b248atagen2026-10-10 08:12 UTC41
    serde_json::from_slice(&to_bytes(response.into_body(), usize::MAX).await.unwrap()).unwrap()
a9b016e4b248atagen2026-10-10 08:12 UTC42
}
a9b016e4b248atagen2026-10-10 08:12 UTC43
782069860236atagen2026-10-10 12:44 UTC44
async fn text(response: Response) -> String {
782069860236atagen2026-10-10 12:44 UTC45
    String::from_utf8(
782069860236atagen2026-10-10 12:44 UTC46
        to_bytes(response.into_body(), usize::MAX)
782069860236atagen2026-10-10 12:44 UTC47
            .await
782069860236atagen2026-10-10 12:44 UTC48
            .unwrap()
782069860236atagen2026-10-10 12:44 UTC49
            .to_vec(),
782069860236atagen2026-10-10 12:44 UTC50
    )
782069860236atagen2026-10-10 12:44 UTC51
    .unwrap()
782069860236atagen2026-10-10 12:44 UTC52
}
782069860236atagen2026-10-10 12:44 UTC53
782069860236atagen2026-10-10 12:44 UTC54
fn header<'a>(response: &'a Response, name: &str) -> &'a str {
782069860236atagen2026-10-10 12:44 UTC55
    response.headers()[name].to_str().unwrap()
782069860236atagen2026-10-10 12:44 UTC56
}
782069860236atagen2026-10-10 12:44 UTC57
782069860236atagen2026-10-10 12:44 UTC58
fn fixture() -> Run {
782069860236atagen2026-10-10 12:44 UTC59
    Run::new("cafebabe".into(), "cafe".into(), 69)
782069860236atagen2026-10-10 12:44 UTC60
}
782069860236atagen2026-10-10 12:44 UTC61
782069860236atagen2026-10-10 12:44 UTC62
fn seeded_with(floor: Floor, setup: impl FnOnce(&mut Run)) -> Router {
782069860236atagen2026-10-10 12:44 UTC63
    let state = Arc::new(AppState::default());
782069860236atagen2026-10-10 12:44 UTC64
    let mut run = fixture();
782069860236atagen2026-10-10 12:44 UTC65
    run.floor = floor;
782069860236atagen2026-10-10 12:44 UTC66
    setup(&mut run);
782069860236atagen2026-10-10 12:44 UTC67
    state.insert(run);
782069860236atagen2026-10-10 12:44 UTC68
    router(state)
782069860236atagen2026-10-10 12:44 UTC69
}
782069860236atagen2026-10-10 12:44 UTC70
dfa38cf4cd4batagen2026-10-10 09:17 UTC71
fn seeded(floor: Floor) -> (Router, String) {
a9b016e4b248atagen2026-10-10 08:12 UTC72
    let state = Arc::new(AppState::default());
782069860236atagen2026-10-10 12:44 UTC73
    let mut run = fixture();
dfa38cf4cd4batagen2026-10-10 09:17 UTC74
    run.floor = floor;
782069860236atagen2026-10-10 12:44 UTC75
    let etag = run.origin_etag();
a9b016e4b248atagen2026-10-10 08:12 UTC76
    state.insert(run);
a9b016e4b248atagen2026-10-10 08:12 UTC77
    (router(state), etag)
a9b016e4b248atagen2026-10-10 08:12 UTC78
}
a9b016e4b248atagen2026-10-10 08:12 UTC79
782069860236atagen2026-10-10 12:44 UTC80
fn app() -> Router {
782069860236atagen2026-10-10 12:44 UTC81
    crate::app(Config::default())
782069860236atagen2026-10-10 12:44 UTC82
}
782069860236atagen2026-10-10 12:44 UTC83
9683736a2c5eatagen2026-10-10 13:15 UTC84
fn mum_at_the_door(run: &mut Run) {
9683736a2c5eatagen2026-10-10 13:15 UTC85
    run.mum = Some(MumVisit {
782069860236atagen2026-10-10 12:44 UTC86
        errand: "She heard a noise.",
782069860236atagen2026-10-10 12:44 UTC87
        nonce: "abc".into(),
782069860236atagen2026-10-10 12:44 UTC88
        strikes: 0,
0d604ee39751atagen2026-10-10 14:15 UTC89
        knocked: false,
782069860236atagen2026-10-10 12:44 UTC90
    });
782069860236atagen2026-10-10 12:44 UTC91
}
782069860236atagen2026-10-10 12:44 UTC92
a9b016e4b248atagen2026-10-10 08:12 UTC93
#[tokio::test]
a9b016e4b248atagen2026-10-10 08:12 UTC94
async fn creating_a_run_returns_location_and_etag() {
782069860236atagen2026-10-10 12:44 UTC95
    let response = send(&app(), "POST", "/runs?seed=1", &[], "").await;
a9b016e4b248atagen2026-10-10 08:12 UTC96
    assert_eq!(response.status(), StatusCode::CREATED);
782069860236atagen2026-10-10 12:44 UTC97
    assert!(header(&response, "location").starts_with("/runs/"));
a9b016e4b248atagen2026-10-10 08:12 UTC98
    assert!(response.headers().contains_key(header::ETAG));
782069860236atagen2026-10-10 12:44 UTC99
    assert_eq!(header(&response, "server"), "jelqd/0.6.9");
782069860236atagen2026-10-10 12:44 UTC100
}
782069860236atagen2026-10-10 12:44 UTC101
782069860236atagen2026-10-10 12:44 UTC102
#[tokio::test]
782069860236atagen2026-10-10 12:44 UTC103
async fn bodies_do_not_repeat_the_headers() {
782069860236atagen2026-10-10 12:44 UTC104
    let (app, _) = seeded(Floor::Bedroom);
782069860236atagen2026-10-10 12:44 UTC105
    let response = send(&app, "GET", "/runs/cafebabe", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC106
    assert!(response.headers().contains_key("x-eq"));
782069860236atagen2026-10-10 12:44 UTC107
    let body = json(response).await;
782069860236atagen2026-10-10 12:44 UTC108
    for stat in [
782069860236atagen2026-10-10 12:44 UTC109
        "eq",
782069860236atagen2026-10-10 12:44 UTC110
        "girth",
782069860236atagen2026-10-10 12:44 UTC111
        "foreskin",
782069860236atagen2026-10-10 12:44 UTC112
        "stamina",
782069860236atagen2026-10-10 12:44 UTC113
        "cash",
782069860236atagen2026-10-10 12:44 UTC114
        "self_reported_length",
782069860236atagen2026-10-10 12:44 UTC115
    ] {
782069860236atagen2026-10-10 12:44 UTC116
        assert!(body.get(stat).is_none(), "{stat} is already a header");
782069860236atagen2026-10-10 12:44 UTC117
    }
a9b016e4b248atagen2026-10-10 08:12 UTC118
}
a9b016e4b248atagen2026-10-10 08:12 UTC119
a9b016e4b248atagen2026-10-10 08:12 UTC120
#[tokio::test]
a9b016e4b248atagen2026-10-10 08:12 UTC121
async fn jelqing_cold_fails_the_precondition() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC122
    let (app, _) = seeded(Floor::Bedroom);
a9b016e4b248atagen2026-10-10 08:12 UTC123
    let response = send(&app, "JELQ", "/runs/cafebabe?technique=stretch", &[], "").await;
a9b016e4b248atagen2026-10-10 08:12 UTC124
    assert_eq!(response.status(), StatusCode::PRECONDITION_FAILED);
a9b016e4b248atagen2026-10-10 08:12 UTC125
    assert_eq!(
782069860236atagen2026-10-10 12:44 UTC126
        header(&response, "content-type"),
a9b016e4b248atagen2026-10-10 08:12 UTC127
        "application/problem+json"
a9b016e4b248atagen2026-10-10 08:12 UTC128
    );
a9b016e4b248atagen2026-10-10 08:12 UTC129
}
a9b016e4b248atagen2026-10-10 08:12 UTC130
a9b016e4b248atagen2026-10-10 08:12 UTC131
#[tokio::test]
a9b016e4b248atagen2026-10-10 08:12 UTC132
async fn wet_without_lube_is_unsupported_media() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC133
    let (app, _) = seeded(Floor::Bedroom);
a9b016e4b248atagen2026-10-10 08:12 UTC134
    send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
a9b016e4b248atagen2026-10-10 08:12 UTC135
    let response = send(&app, "JELQ", "/runs/cafebabe?technique=wet", &[], "").await;
a9b016e4b248atagen2026-10-10 08:12 UTC136
    assert_eq!(response.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
a9b016e4b248atagen2026-10-10 08:12 UTC137
    let images = send(
a9b016e4b248atagen2026-10-10 08:12 UTC138
        &app,
a9b016e4b248atagen2026-10-10 08:12 UTC139
        "JELQ",
a9b016e4b248atagen2026-10-10 08:12 UTC140
        "/runs/cafebabe?technique=wet",
a9b016e4b248atagen2026-10-10 08:12 UTC141
        &[("content-type", "image/jpeg")],
a9b016e4b248atagen2026-10-10 08:12 UTC142
        "",
a9b016e4b248atagen2026-10-10 08:12 UTC143
    )
a9b016e4b248atagen2026-10-10 08:12 UTC144
    .await;
a9b016e4b248atagen2026-10-10 08:12 UTC145
    assert_eq!(json(images).await["detail"], "No.");
a9b016e4b248atagen2026-10-10 08:12 UTC146
}
a9b016e4b248atagen2026-10-10 08:12 UTC147
a9b016e4b248atagen2026-10-10 08:12 UTC148
#[tokio::test]
a9b016e4b248atagen2026-10-10 08:12 UTC149
async fn unknown_methods_are_not_implemented() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC150
    let (app, _) = seeded(Floor::Bedroom);
a9b016e4b248atagen2026-10-10 08:12 UTC151
    let response = send(&app, "MILK", "/runs/cafebabe", &[], "").await;
a9b016e4b248atagen2026-10-10 08:12 UTC152
    assert_eq!(response.status(), StatusCode::NOT_IMPLEMENTED);
782069860236atagen2026-10-10 12:44 UTC153
    let teapot = send(&app, "BREW", "/runs/cafebabe", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC154
    assert_eq!(teapot.status(), StatusCode::IM_A_TEAPOT);
782069860236atagen2026-10-10 12:44 UTC155
}
782069860236atagen2026-10-10 12:44 UTC156
782069860236atagen2026-10-10 12:44 UTC157
#[tokio::test]
782069860236atagen2026-10-10 12:44 UTC158
async fn the_clinic_does_not_advertise_jelq() {
782069860236atagen2026-10-10 12:44 UTC159
    let (app, _) = seeded(Floor::Clinic);
782069860236atagen2026-10-10 12:44 UTC160
    let options = send(&app, "OPTIONS", "/runs/cafebabe", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC161
    assert!(!header(&options, "allow").contains("JELQ"));
782069860236atagen2026-10-10 12:44 UTC162
    let jelq = send(&app, "JELQ", "/runs/cafebabe?technique=wet", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC163
    assert_eq!(jelq.status(), StatusCode::METHOD_NOT_ALLOWED);
782069860236atagen2026-10-10 12:44 UTC164
    assert!(!header(&jelq, "allow").contains("JELQ"));
782069860236atagen2026-10-10 12:44 UTC165
    let (bedroom, _) = seeded(Floor::Bedroom);
782069860236atagen2026-10-10 12:44 UTC166
    let options = send(&bedroom, "OPTIONS", "/runs/cafebabe", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC167
    assert!(header(&options, "allow").contains("JELQ"));
a9b016e4b248atagen2026-10-10 08:12 UTC168
}
a9b016e4b248atagen2026-10-10 08:12 UTC169
a9b016e4b248atagen2026-10-10 08:12 UTC170
#[tokio::test]
a9b016e4b248atagen2026-10-10 08:12 UTC171
async fn patch_only_accepts_the_truth() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC172
    let (app, _) = seeded(Floor::Bedroom);
a9b016e4b248atagen2026-10-10 08:12 UTC173
    let patch = [("content-type", "application/json-patch+json")];
a9b016e4b248atagen2026-10-10 08:12 UTC174
    let replace = send(
a9b016e4b248atagen2026-10-10 08:12 UTC175
        &app,
a9b016e4b248atagen2026-10-10 08:12 UTC176
        "PATCH",
a9b016e4b248atagen2026-10-10 08:12 UTC177
        "/runs/cafebabe",
a9b016e4b248atagen2026-10-10 08:12 UTC178
        &patch,
a9b016e4b248atagen2026-10-10 08:12 UTC179
        r#"[{"op":"replace","path":"/length","value":12}]"#,
a9b016e4b248atagen2026-10-10 08:12 UTC180
    )
a9b016e4b248atagen2026-10-10 08:12 UTC181
    .await;
a9b016e4b248atagen2026-10-10 08:12 UTC182
    assert_eq!(replace.status(), StatusCode::UNPROCESSABLE_ENTITY);
a9b016e4b248atagen2026-10-10 08:12 UTC183
    let lie = send(
a9b016e4b248atagen2026-10-10 08:12 UTC184
        &app,
a9b016e4b248atagen2026-10-10 08:12 UTC185
        "PATCH",
a9b016e4b248atagen2026-10-10 08:12 UTC186
        "/runs/cafebabe",
a9b016e4b248atagen2026-10-10 08:12 UTC187
        &patch,
a9b016e4b248atagen2026-10-10 08:12 UTC188
        r#"[{"op":"test","path":"/length","value":40}]"#,
a9b016e4b248atagen2026-10-10 08:12 UTC189
    )
a9b016e4b248atagen2026-10-10 08:12 UTC190
    .await;
a9b016e4b248atagen2026-10-10 08:12 UTC191
    assert_eq!(
a9b016e4b248atagen2026-10-10 08:12 UTC192
        json(lie).await["type"],
a9b016e4b248atagen2026-10-10 08:12 UTC193
        "https://jelq.dev/problems/exaggeration"
a9b016e4b248atagen2026-10-10 08:12 UTC194
    );
a9b016e4b248atagen2026-10-10 08:12 UTC195
}
a9b016e4b248atagen2026-10-10 08:12 UTC196
a9b016e4b248atagen2026-10-10 08:12 UTC197
#[tokio::test]
a9b016e4b248atagen2026-10-10 08:12 UTC198
async fn deleting_the_foreskin_requires_certainty() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC199
    let (app, _) = seeded(Floor::Bedroom);
a9b016e4b248atagen2026-10-10 08:12 UTC200
    let response = send(&app, "DELETE", "/runs/cafebabe/foreskin", &[], "").await;
a9b016e4b248atagen2026-10-10 08:12 UTC201
    assert_eq!(response.status(), StatusCode::PRECONDITION_REQUIRED);
782069860236atagen2026-10-10 12:44 UTC202
    let look = send(&app, "GET", "/runs/cafebabe/foreskin", &[], "").await;
ec6fd4bb5b60atagen2026-10-11 04:34 UTC203
    assert_eq!(look.status(), StatusCode::OK);
782069860236atagen2026-10-10 12:44 UTC204
    let etag = header(&look, "etag").to_owned();
ec6fd4bb5b60atagen2026-10-11 04:34 UTC205
    assert!(text(look).await.contains('~'));
a9b016e4b248atagen2026-10-10 08:12 UTC206
    let response = send(
a9b016e4b248atagen2026-10-10 08:12 UTC207
        &app,
a9b016e4b248atagen2026-10-10 08:12 UTC208
        "DELETE",
a9b016e4b248atagen2026-10-10 08:12 UTC209
        "/runs/cafebabe/foreskin",
a9b016e4b248atagen2026-10-10 08:12 UTC210
        &[("if-match", &etag)],
a9b016e4b248atagen2026-10-10 08:12 UTC211
        "",
a9b016e4b248atagen2026-10-10 08:12 UTC212
    )
a9b016e4b248atagen2026-10-10 08:12 UTC213
    .await;
a9b016e4b248atagen2026-10-10 08:12 UTC214
    assert_eq!(response.status(), StatusCode::NO_CONTENT);
a9b016e4b248atagen2026-10-10 08:12 UTC215
    let response = send(&app, "GET", "/runs/cafebabe/foreskin", &[], "").await;
a9b016e4b248atagen2026-10-10 08:12 UTC216
    assert_eq!(response.status(), StatusCode::GONE);
a9b016e4b248atagen2026-10-10 08:12 UTC217
}
a9b016e4b248atagen2026-10-10 08:12 UTC218
a9b016e4b248atagen2026-10-10 08:12 UTC219
#[tokio::test]
a9b016e4b248atagen2026-10-10 08:12 UTC220
async fn length_is_chunked_until_the_ruler() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC221
    let (app, _) = seeded(Floor::Bedroom);
a9b016e4b248atagen2026-10-10 08:12 UTC222
    let lie = send(&app, "GET", "/runs/cafebabe/length", &[], "").await;
a9b016e4b248atagen2026-10-10 08:12 UTC223
    assert!(!lie.headers().contains_key(header::CONTENT_LENGTH));
ad00e0e051a1atagen2026-10-10 14:00 UTC224
    let app = seeded_with(Floor::Bedroom, |run| run.items.ruler = true);
a9b016e4b248atagen2026-10-10 08:12 UTC225
    let truth = send(&app, "GET", "/runs/cafebabe/length", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC226
    let declared: usize = header(&truth, "content-length").parse().unwrap();
782069860236atagen2026-10-10 12:44 UTC227
    let body = text(truth).await;
a9b016e4b248atagen2026-10-10 08:12 UTC228
    assert_eq!(body.len(), declared);
782069860236atagen2026-10-10 12:44 UTC229
    assert!(body.starts_with("8=") && body.ends_with('D'));
782069860236atagen2026-10-10 12:44 UTC230
}
782069860236atagen2026-10-10 12:44 UTC231
782069860236atagen2026-10-10 12:44 UTC232
#[tokio::test]
782069860236atagen2026-10-10 12:44 UTC233
async fn length_supports_inch_ranges() {
782069860236atagen2026-10-10 12:44 UTC234
    let (app, _) = seeded(Floor::Bedroom);
782069860236atagen2026-10-10 12:44 UTC235
    let response = send(
782069860236atagen2026-10-10 12:44 UTC236
        &app,
782069860236atagen2026-10-10 12:44 UTC237
        "GET",
782069860236atagen2026-10-10 12:44 UTC238
        "/runs/cafebabe/length",
782069860236atagen2026-10-10 12:44 UTC239
        &[("range", "inches=0-1")],
782069860236atagen2026-10-10 12:44 UTC240
        "",
782069860236atagen2026-10-10 12:44 UTC241
    )
782069860236atagen2026-10-10 12:44 UTC242
    .await;
782069860236atagen2026-10-10 12:44 UTC243
    assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
782069860236atagen2026-10-10 12:44 UTC244
    assert!(header(&response, "content-range").starts_with("inches 0.00-1.00/"));
782069860236atagen2026-10-10 12:44 UTC245
    assert_eq!(text(response).await.len(), 25);
782069860236atagen2026-10-10 12:44 UTC246
    let absurd = send(
782069860236atagen2026-10-10 12:44 UTC247
        &app,
782069860236atagen2026-10-10 12:44 UTC248
        "GET",
782069860236atagen2026-10-10 12:44 UTC249
        "/runs/cafebabe/length",
782069860236atagen2026-10-10 12:44 UTC250
        &[("range", "inches=40-50")],
782069860236atagen2026-10-10 12:44 UTC251
        "",
782069860236atagen2026-10-10 12:44 UTC252
    )
782069860236atagen2026-10-10 12:44 UTC253
    .await;
782069860236atagen2026-10-10 12:44 UTC254
    assert_eq!(absurd.status(), StatusCode::RANGE_NOT_SATISFIABLE);
d46f9d0b953aatagen2026-10-10 14:41 UTC255
    let malformed = send(
d46f9d0b953aatagen2026-10-10 14:41 UTC256
        &app,
d46f9d0b953aatagen2026-10-10 14:41 UTC257
        "GET",
d46f9d0b953aatagen2026-10-10 14:41 UTC258
        "/runs/cafebabe/length",
d46f9d0b953aatagen2026-10-10 14:41 UTC259
        &[("range", "inches=6.17")],
d46f9d0b953aatagen2026-10-10 14:41 UTC260
        "",
d46f9d0b953aatagen2026-10-10 14:41 UTC261
    )
d46f9d0b953aatagen2026-10-10 14:41 UTC262
    .await;
d46f9d0b953aatagen2026-10-10 14:41 UTC263
    assert_eq!(malformed.status(), StatusCode::OK);
d46f9d0b953aatagen2026-10-10 14:41 UTC264
    let suffix = send(
d46f9d0b953aatagen2026-10-10 14:41 UTC265
        &app,
d46f9d0b953aatagen2026-10-10 14:41 UTC266
        "GET",
d46f9d0b953aatagen2026-10-10 14:41 UTC267
        "/runs/cafebabe/length",
d46f9d0b953aatagen2026-10-10 14:41 UTC268
        &[("range", "inches=-1")],
d46f9d0b953aatagen2026-10-10 14:41 UTC269
        "",
d46f9d0b953aatagen2026-10-10 14:41 UTC270
    )
d46f9d0b953aatagen2026-10-10 14:41 UTC271
    .await;
d46f9d0b953aatagen2026-10-10 14:41 UTC272
    assert_eq!(suffix.status(), StatusCode::PARTIAL_CONTENT);
d46f9d0b953aatagen2026-10-10 14:41 UTC273
    assert!(text(suffix).await.ends_with('D'));
d46f9d0b953aatagen2026-10-10 14:41 UTC274
    for (range, status) in [
d46f9d0b953aatagen2026-10-10 14:41 UTC275
        ("inches=1-", StatusCode::PARTIAL_CONTENT),
d46f9d0b953aatagen2026-10-10 14:41 UTC276
        ("inches=40-", StatusCode::RANGE_NOT_SATISFIABLE),
d46f9d0b953aatagen2026-10-10 14:41 UTC277
        ("inches=-0", StatusCode::RANGE_NOT_SATISFIABLE),
d46f9d0b953aatagen2026-10-10 14:41 UTC278
        ("inches=2-1", StatusCode::OK),
d46f9d0b953aatagen2026-10-10 14:41 UTC279
        ("inches=NaN-2", StatusCode::OK),
d46f9d0b953aatagen2026-10-10 14:41 UTC280
        ("inches=0-inf", StatusCode::OK),
d46f9d0b953aatagen2026-10-10 14:41 UTC281
    ] {
d46f9d0b953aatagen2026-10-10 14:41 UTC282
        let response = send(
d46f9d0b953aatagen2026-10-10 14:41 UTC283
            &app,
d46f9d0b953aatagen2026-10-10 14:41 UTC284
            "GET",
d46f9d0b953aatagen2026-10-10 14:41 UTC285
            "/runs/cafebabe/length",
d46f9d0b953aatagen2026-10-10 14:41 UTC286
            &[("range", range)],
d46f9d0b953aatagen2026-10-10 14:41 UTC287
            "",
d46f9d0b953aatagen2026-10-10 14:41 UTC288
        )
d46f9d0b953aatagen2026-10-10 14:41 UTC289
        .await;
d46f9d0b953aatagen2026-10-10 14:41 UTC290
        assert_eq!(response.status(), status, "{range}");
d46f9d0b953aatagen2026-10-10 14:41 UTC291
    }
a9b016e4b248atagen2026-10-10 08:12 UTC292
}
a9b016e4b248atagen2026-10-10 08:12 UTC293
a9b016e4b248atagen2026-10-10 08:12 UTC294
#[tokio::test]
a9b016e4b248atagen2026-10-10 08:12 UTC295
async fn the_clinic_escalates_toward_honesty() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC296
    let (app, _) = seeded(Floor::Clinic);
a9b016e4b248atagen2026-10-10 08:12 UTC297
    let mut hints = Vec::new();
a9b016e4b248atagen2026-10-10 08:12 UTC298
    for _ in 0..3 {
a9b016e4b248atagen2026-10-10 08:12 UTC299
        let response = send(
a9b016e4b248atagen2026-10-10 08:12 UTC300
            &app,
a9b016e4b248atagen2026-10-10 08:12 UTC301
            "GET",
a9b016e4b248atagen2026-10-10 08:12 UTC302
            "/runs/cafebabe/diagnosis",
a9b016e4b248atagen2026-10-10 08:12 UTC303
            &[("authorization", "Excuse \"changing\"")],
a9b016e4b248atagen2026-10-10 08:12 UTC304
            "",
a9b016e4b248atagen2026-10-10 08:12 UTC305
        )
a9b016e4b248atagen2026-10-10 08:12 UTC306
        .await;
a9b016e4b248atagen2026-10-10 08:12 UTC307
        assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
782069860236atagen2026-10-10 12:44 UTC308
        hints.push(header(&response, "www-authenticate").to_owned());
a9b016e4b248atagen2026-10-10 08:12 UTC309
    }
a9b016e4b248atagen2026-10-10 08:12 UTC310
    assert!(hints[2].contains("Be honest."));
a9b016e4b248atagen2026-10-10 08:12 UTC311
}
a9b016e4b248atagen2026-10-10 08:12 UTC312
a9b016e4b248atagen2026-10-10 08:12 UTC313
#[tokio::test]
a9b016e4b248atagen2026-10-10 08:12 UTC314
async fn the_true_ending_is_not_modified() {
782069860236atagen2026-10-10 12:44 UTC315
    let (early, _) = seeded(Floor::Bedroom);
a9b016e4b248atagen2026-10-10 08:12 UTC316
    let response = send(&early, "GET", "/runs/cafebabe/diagnosis", &[], "").await;
a9b016e4b248atagen2026-10-10 08:12 UTC317
    assert_eq!(response.status(), StatusCode::TOO_EARLY);
a9b016e4b248atagen2026-10-10 08:12 UTC318
782069860236atagen2026-10-10 12:44 UTC319
    let (app, etag) = seeded(Floor::Clinic);
782069860236atagen2026-10-10 12:44 UTC320
    assert!(
782069860236atagen2026-10-10 12:44 UTC321
        !etag.contains("cafebabe"),
782069860236atagen2026-10-10 12:44 UTC322
        "the origin ETag must not be derivable from the id"
782069860236atagen2026-10-10 12:44 UTC323
    );
a9b016e4b248atagen2026-10-10 08:12 UTC324
    let response = send(
a9b016e4b248atagen2026-10-10 08:12 UTC325
        &app,
a9b016e4b248atagen2026-10-10 08:12 UTC326
        "GET",
a9b016e4b248atagen2026-10-10 08:12 UTC327
        "/runs/cafebabe/diagnosis",
a9b016e4b248atagen2026-10-10 08:12 UTC328
        &[
a9b016e4b248atagen2026-10-10 08:12 UTC329
            ("authorization", "Bearer honesty"),
a9b016e4b248atagen2026-10-10 08:12 UTC330
            ("if-none-match", &etag),
a9b016e4b248atagen2026-10-10 08:12 UTC331
        ],
a9b016e4b248atagen2026-10-10 08:12 UTC332
        "",
a9b016e4b248atagen2026-10-10 08:12 UTC333
    )
a9b016e4b248atagen2026-10-10 08:12 UTC334
    .await;
a9b016e4b248atagen2026-10-10 08:12 UTC335
    assert_eq!(response.status(), StatusCode::NOT_MODIFIED);
782069860236atagen2026-10-10 12:44 UTC336
    assert_eq!(header(&response, "x-diagnosis"), "normal");
a9b016e4b248atagen2026-10-10 08:12 UTC337
}
a9b016e4b248atagen2026-10-10 08:12 UTC338
a9b016e4b248atagen2026-10-10 08:12 UTC339
#[tokio::test]
9683736a2c5eatagen2026-10-10 13:15 UTC340
async fn mum_accepts_a_known_excuse() {
9683736a2c5eatagen2026-10-10 13:15 UTC341
    let app = seeded_with(Floor::Bedroom, mum_at_the_door);
a9b016e4b248atagen2026-10-10 08:12 UTC342
    let response = send(&app, "GET", "/runs/cafebabe", &[], "").await;
a9b016e4b248atagen2026-10-10 08:12 UTC343
    assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
9683736a2c5eatagen2026-10-10 13:15 UTC344
    assert_eq!(header(&response, "via"), "1.1 mum");
a9b016e4b248atagen2026-10-10 08:12 UTC345
    let response = send(
a9b016e4b248atagen2026-10-10 08:12 UTC346
        &app,
a9b016e4b248atagen2026-10-10 08:12 UTC347
        "GET",
a9b016e4b248atagen2026-10-10 08:12 UTC348
        "/runs/cafebabe",
a9b016e4b248atagen2026-10-10 08:12 UTC349
        &[("authorization", "Excuse \"homework\"")],
a9b016e4b248atagen2026-10-10 08:12 UTC350
        "",
a9b016e4b248atagen2026-10-10 08:12 UTC351
    )
a9b016e4b248atagen2026-10-10 08:12 UTC352
    .await;
a9b016e4b248atagen2026-10-10 08:12 UTC353
    assert_eq!(response.status(), StatusCode::OK);
a9b016e4b248atagen2026-10-10 08:12 UTC354
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC355
782069860236atagen2026-10-10 12:44 UTC356
#[tokio::test]
9683736a2c5eatagen2026-10-10 13:15 UTC357
async fn mum_runs_out_of_patience() {
9683736a2c5eatagen2026-10-10 13:15 UTC358
    let app = seeded_with(Floor::Bedroom, mum_at_the_door);
782069860236atagen2026-10-10 12:44 UTC359
    let lie = [("authorization", "Excuse \"it is for science\"")];
782069860236atagen2026-10-10 12:44 UTC360
    for _ in 0..2 {
782069860236atagen2026-10-10 12:44 UTC361
        let response = send(&app, "GET", "/runs/cafebabe", &lie, "").await;
782069860236atagen2026-10-10 12:44 UTC362
        assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
782069860236atagen2026-10-10 12:44 UTC363
    }
782069860236atagen2026-10-10 12:44 UTC364
    let caught = send(&app, "GET", "/runs/cafebabe", &lie, "").await;
782069860236atagen2026-10-10 12:44 UTC365
    assert_eq!(caught.status(), StatusCode::GONE);
782069860236atagen2026-10-10 12:44 UTC366
    assert_eq!(
782069860236atagen2026-10-10 12:44 UTC367
        json(caught).await["type"],
782069860236atagen2026-10-10 12:44 UTC368
        "https://jelq.dev/problems/caught"
782069860236atagen2026-10-10 12:44 UTC369
    );
782069860236atagen2026-10-10 12:44 UTC370
}
782069860236atagen2026-10-10 12:44 UTC371
782069860236atagen2026-10-10 12:44 UTC372
#[tokio::test]
9683736a2c5eatagen2026-10-10 13:15 UTC373
async fn mum_remembers_excuses_by_christmas() {
9683736a2c5eatagen2026-10-10 13:15 UTC374
    let app = seeded_with(Floor::Nans, |run| {
9683736a2c5eatagen2026-10-10 13:15 UTC375
        mum_at_the_door(run);
782069860236atagen2026-10-10 12:44 UTC376
        run.heard.push("homework".into());
782069860236atagen2026-10-10 12:44 UTC377
    });
782069860236atagen2026-10-10 12:44 UTC378
    let basic = send(
782069860236atagen2026-10-10 12:44 UTC379
        &app,
782069860236atagen2026-10-10 12:44 UTC380
        "GET",
782069860236atagen2026-10-10 12:44 UTC381
        "/runs/cafebabe",
782069860236atagen2026-10-10 12:44 UTC382
        &[("authorization", "Excuse \"changing\"")],
782069860236atagen2026-10-10 12:44 UTC383
        "",
782069860236atagen2026-10-10 12:44 UTC384
    )
782069860236atagen2026-10-10 12:44 UTC385
    .await;
782069860236atagen2026-10-10 12:44 UTC386
    assert_eq!(basic.status(), StatusCode::UNAUTHORIZED);
782069860236atagen2026-10-10 12:44 UTC387
    assert!(header(&basic, "www-authenticate").starts_with("Digest"));
782069860236atagen2026-10-10 12:44 UTC388
    let replay = send(
782069860236atagen2026-10-10 12:44 UTC389
        &app,
782069860236atagen2026-10-10 12:44 UTC390
        "GET",
782069860236atagen2026-10-10 12:44 UTC391
        "/runs/cafebabe",
782069860236atagen2026-10-10 12:44 UTC392
        &[(
782069860236atagen2026-10-10 12:44 UTC393
            "authorization",
782069860236atagen2026-10-10 12:44 UTC394
            "Digest username=\"homework\", nonce=\"abc\"",
782069860236atagen2026-10-10 12:44 UTC395
        )],
782069860236atagen2026-10-10 12:44 UTC396
        "",
782069860236atagen2026-10-10 12:44 UTC397
    )
782069860236atagen2026-10-10 12:44 UTC398
    .await;
782069860236atagen2026-10-10 12:44 UTC399
    assert_eq!(
782069860236atagen2026-10-10 12:44 UTC400
        json(replay).await["type"],
782069860236atagen2026-10-10 12:44 UTC401
        "https://jelq.dev/problems/excuse-replayed"
782069860236atagen2026-10-10 12:44 UTC402
    );
782069860236atagen2026-10-10 12:44 UTC403
    let fresh = send(
782069860236atagen2026-10-10 12:44 UTC404
        &app,
782069860236atagen2026-10-10 12:44 UTC405
        "GET",
782069860236atagen2026-10-10 12:44 UTC406
        "/runs/cafebabe",
782069860236atagen2026-10-10 12:44 UTC407
        &[(
782069860236atagen2026-10-10 12:44 UTC408
            "authorization",
9683736a2c5eatagen2026-10-10 13:15 UTC409
            "Digest username=\"wrapping presents\", nonce=\"abc\"",
782069860236atagen2026-10-10 12:44 UTC410
        )],
782069860236atagen2026-10-10 12:44 UTC411
        "",
782069860236atagen2026-10-10 12:44 UTC412
    )
782069860236atagen2026-10-10 12:44 UTC413
    .await;
782069860236atagen2026-10-10 12:44 UTC414
    assert_eq!(fresh.status(), StatusCode::OK);
782069860236atagen2026-10-10 12:44 UTC415
}
782069860236atagen2026-10-10 12:44 UTC416
782069860236atagen2026-10-10 12:44 UTC417
#[tokio::test]
782069860236atagen2026-10-10 12:44 UTC418
async fn a_pulled_muscle_is_a_true_excuse() {
9683736a2c5eatagen2026-10-10 13:15 UTC419
    let app = seeded_with(Floor::Nans, |run| {
9683736a2c5eatagen2026-10-10 13:15 UTC420
        mum_at_the_door(run);
782069860236atagen2026-10-10 12:44 UTC421
        run.conditions.pulled_muscle = true;
782069860236atagen2026-10-10 12:44 UTC422
        run.heard.push("pulled a muscle".into());
782069860236atagen2026-10-10 12:44 UTC423
    });
782069860236atagen2026-10-10 12:44 UTC424
    let response = send(
782069860236atagen2026-10-10 12:44 UTC425
        &app,
782069860236atagen2026-10-10 12:44 UTC426
        "GET",
782069860236atagen2026-10-10 12:44 UTC427
        "/runs/cafebabe",
782069860236atagen2026-10-10 12:44 UTC428
        &[(
782069860236atagen2026-10-10 12:44 UTC429
            "authorization",
782069860236atagen2026-10-10 12:44 UTC430
            "Digest username=\"pulled a muscle\", nonce=\"abc\"",
782069860236atagen2026-10-10 12:44 UTC431
        )],
782069860236atagen2026-10-10 12:44 UTC432
        "",
782069860236atagen2026-10-10 12:44 UTC433
    )
782069860236atagen2026-10-10 12:44 UTC434
    .await;
782069860236atagen2026-10-10 12:44 UTC435
    assert_eq!(response.status(), StatusCode::OK);
782069860236atagen2026-10-10 12:44 UTC436
    assert!(header(&response, "x-stamina").ends_with("/7"));
dfa38cf4cd4batagen2026-10-10 09:17 UTC437
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC438
dfa38cf4cd4batagen2026-10-10 09:17 UTC439
#[tokio::test]
dfa38cf4cd4batagen2026-10-10 09:17 UTC440
async fn floors_end_when_they_end() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC441
    let app = seeded_with(Floor::Bedroom, |_| {});
dfa38cf4cd4batagen2026-10-10 09:17 UTC442
    let early = send(&app, "POST", "/runs/cafebabe/floor", &[], "").await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC443
    assert_eq!(early.status(), StatusCode::TOO_EARLY);
782069860236atagen2026-10-10 12:44 UTC444
    let app = seeded_with(Floor::Bedroom, |run| run.here.turns = 6);
dfa38cf4cd4batagen2026-10-10 09:17 UTC445
    let next = send(&app, "POST", "/runs/cafebabe/floor", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC446
    assert!(
782069860236atagen2026-10-10 12:44 UTC447
        next.status() == StatusCode::SEE_OTHER || next.status() == StatusCode::CONFLICT,
9683736a2c5eatagen2026-10-10 13:15 UTC448
        "either the floor changed or the zip did"
782069860236atagen2026-10-10 12:44 UTC449
    );
dfa38cf4cd4batagen2026-10-10 09:17 UTC450
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC451
dfa38cf4cd4batagen2026-10-10 09:17 UTC452
#[tokio::test]
dfa38cf4cd4batagen2026-10-10 09:17 UTC453
async fn pump_bro_upsells() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC454
    let closed = seeded_with(Floor::Bedroom, |_| {});
dfa38cf4cd4batagen2026-10-10 09:17 UTC455
    let response = send(&closed, "GET", "/runs/cafebabe/shop", &[], "").await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC456
    assert_eq!(response.status(), StatusCode::NOT_FOUND);
dfa38cf4cd4batagen2026-10-10 09:17 UTC457
9683736a2c5eatagen2026-10-10 13:15 UTC458
    let app = seeded_with(Floor::ChangingRoom, |_| {});
dfa38cf4cd4batagen2026-10-10 09:17 UTC459
    let pump = send(&app, "POST", "/runs/cafebabe/shop/pump", &[], "").await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC460
    assert_eq!(pump.status(), StatusCode::UPGRADE_REQUIRED);
782069860236atagen2026-10-10 12:44 UTC461
    assert_eq!(header(&pump, "upgrade"), "pump-pro/2.0");
dfa38cf4cd4batagen2026-10-10 09:17 UTC462
    let pro = send(&app, "POST", "/runs/cafebabe/shop/pump-pro", &[], "").await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC463
    assert_eq!(pro.status(), StatusCode::CREATED);
dfa38cf4cd4batagen2026-10-10 09:17 UTC464
    let broke = send(&app, "POST", "/runs/cafebabe/shop/extender", &[], "").await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC465
    assert_eq!(broke.status(), StatusCode::PAYMENT_REQUIRED);
dfa38cf4cd4batagen2026-10-10 09:17 UTC466
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC467
dfa38cf4cd4batagen2026-10-10 09:17 UTC468
#[tokio::test]
782069860236atagen2026-10-10 12:44 UTC469
async fn only_the_key_opens_the_cage() {
9683736a2c5eatagen2026-10-10 13:15 UTC470
    let app = seeded_with(Floor::ChangingRoom, |run| run.items.cage = true);
782069860236atagen2026-10-10 12:44 UTC471
    send(&app, "POST", "/runs/cafebabe/shop/ice-pack", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC472
    let still = send(
782069860236atagen2026-10-10 12:44 UTC473
        &app,
782069860236atagen2026-10-10 12:44 UTC474
        "GET",
782069860236atagen2026-10-10 12:44 UTC475
        "/runs/cafebabe/inventory/chastity-cage",
782069860236atagen2026-10-10 12:44 UTC476
        &[],
782069860236atagen2026-10-10 12:44 UTC477
        "",
782069860236atagen2026-10-10 12:44 UTC478
    )
782069860236atagen2026-10-10 12:44 UTC479
    .await;
782069860236atagen2026-10-10 12:44 UTC480
    assert_eq!(still.status(), StatusCode::OK);
782069860236atagen2026-10-10 12:44 UTC481
    send(&app, "POST", "/runs/cafebabe/shop/cage-key", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC482
    let gone = send(
782069860236atagen2026-10-10 12:44 UTC483
        &app,
782069860236atagen2026-10-10 12:44 UTC484
        "GET",
782069860236atagen2026-10-10 12:44 UTC485
        "/runs/cafebabe/inventory/chastity-cage",
782069860236atagen2026-10-10 12:44 UTC486
        &[],
782069860236atagen2026-10-10 12:44 UTC487
        "",
782069860236atagen2026-10-10 12:44 UTC488
    )
782069860236atagen2026-10-10 12:44 UTC489
    .await;
782069860236atagen2026-10-10 12:44 UTC490
    assert_eq!(gone.status(), StatusCode::NOT_FOUND);
782069860236atagen2026-10-10 12:44 UTC491
}
782069860236atagen2026-10-10 12:44 UTC492
782069860236atagen2026-10-10 12:44 UTC493
#[tokio::test]
dfa38cf4cd4batagen2026-10-10 09:17 UTC494
async fn combos_are_multi_status_xml() {
782069860236atagen2026-10-10 12:44 UTC495
    let app = seeded_with(Floor::Bathroom, |run| {
782069860236atagen2026-10-10 12:44 UTC496
        run.foreskin = Some(1.0);
782069860236atagen2026-10-10 12:44 UTC497
        run.items.blinds = true;
782069860236atagen2026-10-10 12:44 UTC498
    });
dfa38cf4cd4batagen2026-10-10 09:17 UTC499
    send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC500
    let response = send(
dfa38cf4cd4batagen2026-10-10 09:17 UTC501
        &app,
dfa38cf4cd4batagen2026-10-10 09:17 UTC502
        "JELQ",
dfa38cf4cd4batagen2026-10-10 09:17 UTC503
        "/runs/cafebabe?technique=wet,stretch",
dfa38cf4cd4batagen2026-10-10 09:17 UTC504
        &[("content-type", "application/baby-oil")],
dfa38cf4cd4batagen2026-10-10 09:17 UTC505
        "",
dfa38cf4cd4batagen2026-10-10 09:17 UTC506
    )
dfa38cf4cd4batagen2026-10-10 09:17 UTC507
    .await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC508
    assert_eq!(response.status(), StatusCode::MULTI_STATUS);
782069860236atagen2026-10-10 12:44 UTC509
    assert!(text(response).await.contains("<D:multistatus"));
dfa38cf4cd4batagen2026-10-10 09:17 UTC510
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC511
dfa38cf4cd4batagen2026-10-10 09:17 UTC512
#[tokio::test]
dfa38cf4cd4batagen2026-10-10 09:17 UTC513
async fn the_elder_is_stale() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC514
    let app = seeded_with(Floor::Forum, |run| run.length = 5.0);
dfa38cf4cd4batagen2026-10-10 09:17 UTC515
    let unverified = send(&app, "GET", "/runs/cafebabe/elder", &[], "").await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC516
    assert_eq!(
dfa38cf4cd4batagen2026-10-10 09:17 UTC517
        unverified.status(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC518
        StatusCode::PROXY_AUTHENTICATION_REQUIRED
dfa38cf4cd4batagen2026-10-10 09:17 UTC519
    );
dfa38cf4cd4batagen2026-10-10 09:17 UTC520
    let lie = send(
dfa38cf4cd4batagen2026-10-10 09:17 UTC521
        &app,
dfa38cf4cd4batagen2026-10-10 09:17 UTC522
        "GET",
dfa38cf4cd4batagen2026-10-10 09:17 UTC523
        "/runs/cafebabe/elder",
dfa38cf4cd4batagen2026-10-10 09:17 UTC524
        &[("proxy-authorization", "Ruler 9")],
dfa38cf4cd4batagen2026-10-10 09:17 UTC525
        "",
dfa38cf4cd4batagen2026-10-10 09:17 UTC526
    )
dfa38cf4cd4batagen2026-10-10 09:17 UTC527
    .await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC528
    assert_eq!(lie.status(), StatusCode::BAD_REQUEST);
dfa38cf4cd4batagen2026-10-10 09:17 UTC529
    let csv = send(
dfa38cf4cd4batagen2026-10-10 09:17 UTC530
        &app,
dfa38cf4cd4batagen2026-10-10 09:17 UTC531
        "GET",
dfa38cf4cd4batagen2026-10-10 09:17 UTC532
        "/runs/cafebabe/elder",
dfa38cf4cd4batagen2026-10-10 09:17 UTC533
        &[("proxy-authorization", "Ruler 5.0")],
dfa38cf4cd4batagen2026-10-10 09:17 UTC534
        "",
dfa38cf4cd4batagen2026-10-10 09:17 UTC535
    )
dfa38cf4cd4batagen2026-10-10 09:17 UTC536
    .await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC537
    assert_eq!(csv.status(), StatusCode::OK);
dfa38cf4cd4batagen2026-10-10 09:17 UTC538
    let stale = send(
dfa38cf4cd4batagen2026-10-10 09:17 UTC539
        &app,
dfa38cf4cd4batagen2026-10-10 09:17 UTC540
        "GET",
dfa38cf4cd4batagen2026-10-10 09:17 UTC541
        "/runs/cafebabe/elder",
dfa38cf4cd4batagen2026-10-10 09:17 UTC542
        &[("if-modified-since", "Mon, 15 Jun 2009 00:00:00 GMT")],
dfa38cf4cd4batagen2026-10-10 09:17 UTC543
        "",
dfa38cf4cd4batagen2026-10-10 09:17 UTC544
    )
dfa38cf4cd4batagen2026-10-10 09:17 UTC545
    .await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC546
    assert_eq!(stale.status(), StatusCode::NOT_MODIFIED);
782069860236atagen2026-10-10 12:44 UTC547
}
782069860236atagen2026-10-10 12:44 UTC548
782069860236atagen2026-10-10 12:44 UTC549
#[tokio::test]
782069860236atagen2026-10-10 12:44 UTC550
async fn the_displayed_length_is_accepted_as_honest() {
782069860236atagen2026-10-10 12:44 UTC551
    let app = seeded_with(Floor::Forum, |run| run.length = 5.2496);
782069860236atagen2026-10-10 12:44 UTC552
    let response = send(
782069860236atagen2026-10-10 12:44 UTC553
        &app,
782069860236atagen2026-10-10 12:44 UTC554
        "GET",
782069860236atagen2026-10-10 12:44 UTC555
        "/runs/cafebabe/elder",
782069860236atagen2026-10-10 12:44 UTC556
        &[("proxy-authorization", "Ruler 5.25")],
782069860236atagen2026-10-10 12:44 UTC557
        "",
782069860236atagen2026-10-10 12:44 UTC558
    )
782069860236atagen2026-10-10 12:44 UTC559
    .await;
782069860236atagen2026-10-10 12:44 UTC560
    assert_eq!(response.status(), StatusCode::OK);
dfa38cf4cd4batagen2026-10-10 09:17 UTC561
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC562
dfa38cf4cd4batagen2026-10-10 09:17 UTC563
#[tokio::test]
dfa38cf4cd4batagen2026-10-10 09:17 UTC564
async fn the_hotel_has_a_captive_portal() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC565
    let app = seeded_with(Floor::Hotel, |_| {});
dfa38cf4cd4batagen2026-10-10 09:17 UTC566
    let blocked = send(&app, "GET", "/runs/cafebabe", &[], "").await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC567
    assert_eq!(
dfa38cf4cd4batagen2026-10-10 09:17 UTC568
        blocked.status(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC569
        StatusCode::NETWORK_AUTHENTICATION_REQUIRED
dfa38cf4cd4batagen2026-10-10 09:17 UTC570
    );
dfa38cf4cd4batagen2026-10-10 09:17 UTC571
    let accepted = send(&app, "POST", "/runs/cafebabe/portal", &[], "").await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC572
    assert_eq!(accepted.status(), StatusCode::SEE_OTHER);
dfa38cf4cd4batagen2026-10-10 09:17 UTC573
    let online = send(&app, "GET", "/runs/cafebabe", &[], "").await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC574
    assert_eq!(online.status(), StatusCode::OK);
dfa38cf4cd4batagen2026-10-10 09:17 UTC575
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC576
dfa38cf4cd4batagen2026-10-10 09:17 UTC577
#[tokio::test]
782069860236atagen2026-10-10 12:44 UTC578
async fn the_internet_never_forgets() {
782069860236atagen2026-10-10 12:44 UTC579
    let app = seeded_with(Floor::Bedroom, |run| {
782069860236atagen2026-10-10 12:44 UTC580
        run.filmed_at = Some(std::time::SystemTime::now());
dfa38cf4cd4batagen2026-10-10 09:17 UTC581
    });
782069860236atagen2026-10-10 12:44 UTC582
    let footage = send(&app, "GET", "/runs/cafebabe/footage", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC583
    assert_eq!(footage.status(), StatusCode::OK);
99524cf9f17catagen2026-10-10 10:53 UTC584
    assert_eq!(
782069860236atagen2026-10-10 12:44 UTC585
        header(&footage, "cache-control"),
782069860236atagen2026-10-10 12:44 UTC586
        "public, max-age=31536000, immutable"
99524cf9f17catagen2026-10-10 10:53 UTC587
    );
782069860236atagen2026-10-10 12:44 UTC588
    assert!(footage.headers().contains_key(header::AGE));
782069860236atagen2026-10-10 12:44 UTC589
    let delete = send(&app, "DELETE", "/runs/cafebabe/footage", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC590
    assert_eq!(delete.status(), StatusCode::METHOD_NOT_ALLOWED);
782069860236atagen2026-10-10 12:44 UTC591
    assert!(!header(&delete, "allow").contains("DELETE"));
99524cf9f17catagen2026-10-10 10:53 UTC592
}
99524cf9f17catagen2026-10-10 10:53 UTC593
99524cf9f17catagen2026-10-10 10:53 UTC594
#[tokio::test]
99524cf9f17catagen2026-10-10 10:53 UTC595
async fn forged_ruler_cookies_are_lies() {
99524cf9f17catagen2026-10-10 10:53 UTC596
    let forged = send(
782069860236atagen2026-10-10 12:44 UTC597
        &app(),
99524cf9f17catagen2026-10-10 10:53 UTC598
        "GET",
99524cf9f17catagen2026-10-10 10:53 UTC599
        "/leaderboard?sort=content-length",
99524cf9f17catagen2026-10-10 10:53 UTC600
        &[("cookie", "ruler=1")],
99524cf9f17catagen2026-10-10 10:53 UTC601
        "",
99524cf9f17catagen2026-10-10 10:53 UTC602
    )
99524cf9f17catagen2026-10-10 10:53 UTC603
    .await;
99524cf9f17catagen2026-10-10 10:53 UTC604
    assert_eq!(forged.status(), StatusCode::BAD_REQUEST);
99524cf9f17catagen2026-10-10 10:53 UTC605
}
99524cf9f17catagen2026-10-10 10:53 UTC606
99524cf9f17catagen2026-10-10 10:53 UTC607
#[tokio::test]
782069860236atagen2026-10-10 12:44 UTC608
async fn listings_never_reveal_the_private_id() {
782069860236atagen2026-10-10 12:44 UTC609
    let app = app();
782069860236atagen2026-10-10 12:44 UTC610
    let created = send(&app, "POST", "/runs", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC611
    let location = header(&created, "location").to_owned();
782069860236atagen2026-10-10 12:44 UTC612
    let board = text(send(&app, "GET", "/leaderboard", &[], "").await).await;
782069860236atagen2026-10-10 12:44 UTC613
    let private = location.trim_start_matches("/runs/");
782069860236atagen2026-10-10 12:44 UTC614
    assert!(!board.contains(private));
782069860236atagen2026-10-10 12:44 UTC615
    let entry: Value = serde_json::from_str(&board).unwrap();
782069860236atagen2026-10-10 12:44 UTC616
    let public = entry["entries"][0]["public_id"].as_str().unwrap();
782069860236atagen2026-10-10 12:44 UTC617
    let guess = send(&app, "DELETE", &format!("/runs/{public}"), &[], "").await;
782069860236atagen2026-10-10 12:44 UTC618
    assert_eq!(guess.status(), StatusCode::NOT_FOUND);
782069860236atagen2026-10-10 12:44 UTC619
}
782069860236atagen2026-10-10 12:44 UTC620
782069860236atagen2026-10-10 12:44 UTC621
#[tokio::test]
99524cf9f17catagen2026-10-10 10:53 UTC622
async fn seeded_runs_are_unranked() {
782069860236atagen2026-10-10 12:44 UTC623
    let app = app();
99524cf9f17catagen2026-10-10 10:53 UTC624
    send(&app, "POST", "/runs?seed=7", &[], "").await;
99524cf9f17catagen2026-10-10 10:53 UTC625
    let board = json(send(&app, "GET", "/leaderboard", &[], "").await).await;
99524cf9f17catagen2026-10-10 10:53 UTC626
    assert_eq!(board["entries"].as_array().unwrap().len(), 0);
99524cf9f17catagen2026-10-10 10:53 UTC627
    let all = json(send(&app, "GET", "/leaderboard?seeded=include", &[], "").await).await;
99524cf9f17catagen2026-10-10 10:53 UTC628
    assert_eq!(all["entries"][0]["ranked"], false);
99524cf9f17catagen2026-10-10 10:53 UTC629
}
99524cf9f17catagen2026-10-10 10:53 UTC630
99524cf9f17catagen2026-10-10 10:53 UTC631
#[tokio::test]
782069860236atagen2026-10-10 12:44 UTC632
async fn idle_runs_are_evicted_and_the_server_has_a_limit() {
782069860236atagen2026-10-10 12:44 UTC633
    let forgetful = crate::app(Config {
782069860236atagen2026-10-10 12:44 UTC634
        idle_ttl: Duration::ZERO,
782069860236atagen2026-10-10 12:44 UTC635
        ..Config::default()
782069860236atagen2026-10-10 12:44 UTC636
    });
782069860236atagen2026-10-10 12:44 UTC637
    let first = send(&forgetful, "POST", "/runs", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC638
    let location = header(&first, "location").to_owned();
782069860236atagen2026-10-10 12:44 UTC639
    send(&forgetful, "POST", "/runs", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC640
    let gone = send(&forgetful, "GET", &location, &[], "").await;
782069860236atagen2026-10-10 12:44 UTC641
    assert_eq!(gone.status(), StatusCode::NOT_FOUND);
782069860236atagen2026-10-10 12:44 UTC642
782069860236atagen2026-10-10 12:44 UTC643
    let tiny = crate::app(Config {
782069860236atagen2026-10-10 12:44 UTC644
        max_runs: 1,
782069860236atagen2026-10-10 12:44 UTC645
        ..Config::default()
782069860236atagen2026-10-10 12:44 UTC646
    });
782069860236atagen2026-10-10 12:44 UTC647
    send(&tiny, "POST", "/runs", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC648
    let full = send(&tiny, "POST", "/runs", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC649
    assert_eq!(full.status(), StatusCode::SERVICE_UNAVAILABLE);
782069860236atagen2026-10-10 12:44 UTC650
}
782069860236atagen2026-10-10 12:44 UTC651
782069860236atagen2026-10-10 12:44 UTC652
#[tokio::test]
782069860236atagen2026-10-10 12:44 UTC653
async fn shame_from_two_runs_adds_up() {
782069860236atagen2026-10-10 12:44 UTC654
    let state = Arc::new(AppState::default());
782069860236atagen2026-10-10 12:44 UTC655
    state.insert(fixture());
782069860236atagen2026-10-10 12:44 UTC656
    state.insert(Run::new("deadbeef".into(), "dead".into(), 7));
782069860236atagen2026-10-10 12:44 UTC657
    let app = router(state);
782069860236atagen2026-10-10 12:44 UTC658
    let first = send(&app, "TRACE", "/runs/cafebabe", &[], "").await;
782069860236atagen2026-10-10 12:44 UTC659
    assert_eq!(header(&first, "x-shame"), "1");
782069860236atagen2026-10-10 12:44 UTC660
    let cookie = header(&first, "set-cookie")
782069860236atagen2026-10-10 12:44 UTC661
        .split(';')
782069860236atagen2026-10-10 12:44 UTC662
        .next()
782069860236atagen2026-10-10 12:44 UTC663
        .unwrap()
782069860236atagen2026-10-10 12:44 UTC664
        .to_owned();
782069860236atagen2026-10-10 12:44 UTC665
    let second = send(&app, "TRACE", "/runs/deadbeef", &[("cookie", &cookie)], "").await;
782069860236atagen2026-10-10 12:44 UTC666
    assert_eq!(header(&second, "x-shame"), "2");
782069860236atagen2026-10-10 12:44 UTC667
}
782069860236atagen2026-10-10 12:44 UTC668
782069860236atagen2026-10-10 12:44 UTC669
#[tokio::test]
99524cf9f17catagen2026-10-10 10:53 UTC670
async fn the_root_negotiates() {
782069860236atagen2026-10-10 12:44 UTC671
    let app = app();
99524cf9f17catagen2026-10-10 10:53 UTC672
    let browser = send(&app, "GET", "/", &[("accept", "text/html,*/*")], "").await;
99524cf9f17catagen2026-10-10 10:53 UTC673
    assert_eq!(browser.status(), StatusCode::SEE_OTHER);
782069860236atagen2026-10-10 12:44 UTC674
    assert_eq!(header(&browser, "location"), "/ui/");
99524cf9f17catagen2026-10-10 10:53 UTC675
    let curl = send(&app, "GET", "/", &[("accept", "*/*")], "").await;
99524cf9f17catagen2026-10-10 10:53 UTC676
    assert_eq!(json(curl).await["ui"], "/ui/");
99524cf9f17catagen2026-10-10 10:53 UTC677
    let page = send(&app, "GET", "/ui/", &[], "").await;
99524cf9f17catagen2026-10-10 10:53 UTC678
    assert_eq!(page.status(), StatusCode::OK);
99524cf9f17catagen2026-10-10 10:53 UTC679
}
99524cf9f17catagen2026-10-10 10:53 UTC680
99524cf9f17catagen2026-10-10 10:53 UTC681
#[tokio::test]
99524cf9f17catagen2026-10-10 10:53 UTC682
async fn the_ui_replays_what_browsers_cannot_send() {
782069860236atagen2026-10-10 12:44 UTC683
    let app = app();
99524cf9f17catagen2026-10-10 10:53 UTC684
    let envelope =
99524cf9f17catagen2026-10-10 10:53 UTC685
        r#"{"method": "TRACE", "path": "/graveyard", "headers": [["X-Confession", "I did it"]]}"#;
99524cf9f17catagen2026-10-10 10:53 UTC686
    let traced = json(send(&app, "POST", "/ui/send", &[], envelope).await).await;
99524cf9f17catagen2026-10-10 10:53 UTC687
    assert_eq!(traced["status"], 405);
99524cf9f17catagen2026-10-10 10:53 UTC688
    let envelope = r#"{"method": "GET", "path": "/", "headers": [["Accept", "text/html"]]}"#;
99524cf9f17catagen2026-10-10 10:53 UTC689
    let redirected = json(send(&app, "POST", "/ui/send", &[], envelope).await).await;
99524cf9f17catagen2026-10-10 10:53 UTC690
    assert_eq!(redirected["status"], 303);
99524cf9f17catagen2026-10-10 10:53 UTC691
    assert_eq!(redirected["reason"], "See Other");
99524cf9f17catagen2026-10-10 10:53 UTC692
}
c3e0c27cf8fdatagen2026-10-10 11:28 UTC693
c3e0c27cf8fdatagen2026-10-10 11:28 UTC694
#[tokio::test]
ba879f96510datagen2026-10-10 12:03 UTC695
async fn the_ui_decodes_gzip_for_display() {
ba879f96510datagen2026-10-10 12:03 UTC696
    let app = seeded_with(Floor::Bedroom, |run| run.items.ring = true);
ba879f96510datagen2026-10-10 12:03 UTC697
    let envelope =
ba879f96510datagen2026-10-10 12:03 UTC698
        r#"{"method": "GET", "path": "/runs/cafebabe", "headers": [["Accept-Encoding", "gzip"]]}"#;
ba879f96510datagen2026-10-10 12:03 UTC699
    let shown = json(send(&app, "POST", "/ui/send", &[], envelope).await).await;
ba879f96510datagen2026-10-10 12:03 UTC700
    assert!(
ba879f96510datagen2026-10-10 12:03 UTC701
        shown["body"]
ba879f96510datagen2026-10-10 12:03 UTC702
            .as_str()
ba879f96510datagen2026-10-10 12:03 UTC703
            .unwrap()
782069860236atagen2026-10-10 12:44 UTC704
            .contains("\"public_id\":\"cafe\"")
ba879f96510datagen2026-10-10 12:03 UTC705
    );
ba879f96510datagen2026-10-10 12:03 UTC706
    let encoding = shown["headers"]
ba879f96510datagen2026-10-10 12:03 UTC707
        .as_array()
ba879f96510datagen2026-10-10 12:03 UTC708
        .unwrap()
ba879f96510datagen2026-10-10 12:03 UTC709
        .iter()
ba879f96510datagen2026-10-10 12:03 UTC710
        .any(|h| h[0] == "content-encoding" && h[1] == "gzip");
ba879f96510datagen2026-10-10 12:03 UTC711
    assert!(encoding);
ba879f96510datagen2026-10-10 12:03 UTC712
}
782069860236atagen2026-10-10 12:44 UTC713
782069860236atagen2026-10-10 12:44 UTC714
#[tokio::test]
782069860236atagen2026-10-10 12:44 UTC715
async fn http_1_0_gets_the_pdf_treatment() {
782069860236atagen2026-10-10 12:44 UTC716
    let request = Request::builder()
782069860236atagen2026-10-10 12:44 UTC717
        .uri("/graveyard")
782069860236atagen2026-10-10 12:44 UTC718
        .version(Version::HTTP_10)
782069860236atagen2026-10-10 12:44 UTC719
        .body(Body::empty())
782069860236atagen2026-10-10 12:44 UTC720
        .unwrap();
782069860236atagen2026-10-10 12:44 UTC721
    let response = app().oneshot(request).await.unwrap();
782069860236atagen2026-10-10 12:44 UTC722
    assert_eq!(response.status(), StatusCode::HTTP_VERSION_NOT_SUPPORTED);
782069860236atagen2026-10-10 12:44 UTC723
}
ef7d6dee5976atagen2026-10-10 13:23 UTC724
ef7d6dee5976atagen2026-10-10 13:23 UTC725
#[tokio::test]
ef7d6dee5976atagen2026-10-10 13:23 UTC726
async fn mom_speaks_american() {
ef7d6dee5976atagen2026-10-10 13:23 UTC727
    let app = seeded_with(Floor::Nans, mum_at_the_door);
ef7d6dee5976atagen2026-10-10 13:23 UTC728
    let us = [("accept-language", "en-US")];
ef7d6dee5976atagen2026-10-10 13:23 UTC729
    let knock = send(&app, "GET", "/runs/cafebabe", &us, "").await;
ef7d6dee5976atagen2026-10-10 13:23 UTC730
    assert_eq!(header(&knock, "content-language"), "en-US");
ef7d6dee5976atagen2026-10-10 13:23 UTC731
    assert_eq!(header(&knock, "via"), "1.1 mom");
ef7d6dee5976atagen2026-10-10 13:23 UTC732
    assert!(
ef7d6dee5976atagen2026-10-10 13:23 UTC733
        json(knock).await["detail"]
ef7d6dee5976atagen2026-10-10 13:23 UTC734
            .as_str()
ef7d6dee5976atagen2026-10-10 13:23 UTC735
            .unwrap()
ef7d6dee5976atagen2026-10-10 13:23 UTC736
            .contains("Someone is at the door.")
ef7d6dee5976atagen2026-10-10 13:23 UTC737
    );
ef7d6dee5976atagen2026-10-10 13:23 UTC738
    let excuse = [
ef7d6dee5976atagen2026-10-10 13:23 UTC739
        ("accept-language", "en-US"),
ef7d6dee5976atagen2026-10-10 13:23 UTC740
        (
ef7d6dee5976atagen2026-10-10 13:23 UTC741
            "authorization",
ef7d6dee5976atagen2026-10-10 13:23 UTC742
            "Digest username=\"carving the turkey\", nonce=\"abc\"",
ef7d6dee5976atagen2026-10-10 13:23 UTC743
        ),
ef7d6dee5976atagen2026-10-10 13:23 UTC744
    ];
ef7d6dee5976atagen2026-10-10 13:23 UTC745
    let answered = send(&app, "GET", "/runs/cafebabe", &excuse, "").await;
ef7d6dee5976atagen2026-10-10 13:23 UTC746
    assert_eq!(answered.status(), StatusCode::OK);
ef7d6dee5976atagen2026-10-10 13:23 UTC747
    let floor = json(send(&app, "GET", "/runs/cafebabe/floor", &us, "").await).await;
ef7d6dee5976atagen2026-10-10 13:23 UTC748
    assert_eq!(floor["name"], "Grandma's house for Thanksgiving");
ef7d6dee5976atagen2026-10-10 13:23 UTC749
}
ef7d6dee5976atagen2026-10-10 13:23 UTC750
ef7d6dee5976atagen2026-10-10 13:23 UTC751
#[tokio::test]
ef7d6dee5976atagen2026-10-10 13:23 UTC752
async fn the_default_locale_is_british() {
ef7d6dee5976atagen2026-10-10 13:23 UTC753
    let (app, _) = seeded(Floor::Nans);
ef7d6dee5976atagen2026-10-10 13:23 UTC754
    let floor = send(&app, "GET", "/runs/cafebabe/floor", &[], "").await;
ef7d6dee5976atagen2026-10-10 13:23 UTC755
    assert_eq!(header(&floor, "content-language"), "en-GB");
ef7d6dee5976atagen2026-10-10 13:23 UTC756
    assert!(header(&floor, "vary").contains("Accept-Language"));
ef7d6dee5976atagen2026-10-10 13:23 UTC757
    assert_eq!(json(floor).await["name"], "Nan's house at Christmas");
ef7d6dee5976atagen2026-10-10 13:23 UTC758
}
58e1fb3943e6atagen2026-10-10 13:30 UTC759
58e1fb3943e6atagen2026-10-10 13:30 UTC760
fn advertised(response: &Response) -> Vec<String> {
58e1fb3943e6atagen2026-10-10 13:30 UTC761
    header(response, "link")
58e1fb3943e6atagen2026-10-10 13:30 UTC762
        .split(", <")
58e1fb3943e6atagen2026-10-10 13:30 UTC763
        .filter(|entry| entry.contains("rels/technique"))
58e1fb3943e6atagen2026-10-10 13:30 UTC764
        .filter_map(|entry| {
58e1fb3943e6atagen2026-10-10 13:30 UTC765
            entry
58e1fb3943e6atagen2026-10-10 13:30 UTC766
                .split("title=\"")
58e1fb3943e6atagen2026-10-10 13:30 UTC767
                .nth(1)?
58e1fb3943e6atagen2026-10-10 13:30 UTC768
                .split('"')
58e1fb3943e6atagen2026-10-10 13:30 UTC769
                .next()
58e1fb3943e6atagen2026-10-10 13:30 UTC770
                .map(str::to_owned)
58e1fb3943e6atagen2026-10-10 13:30 UTC771
        })
58e1fb3943e6atagen2026-10-10 13:30 UTC772
        .collect()
58e1fb3943e6atagen2026-10-10 13:30 UTC773
}
58e1fb3943e6atagen2026-10-10 13:30 UTC774
58e1fb3943e6atagen2026-10-10 13:30 UTC775
#[tokio::test]
58e1fb3943e6atagen2026-10-10 13:30 UTC776
async fn links_offer_only_usable_techniques() {
58e1fb3943e6atagen2026-10-10 13:30 UTC777
    let app = seeded_with(Floor::Bedroom, |run| run.foreskin = Some(1.0));
58e1fb3943e6atagen2026-10-10 13:30 UTC778
    let cold = send(&app, "GET", "/runs/cafebabe", &[], "").await;
b85a0d4536c3atagen2026-10-11 08:50 UTC779
    assert_eq!(advertised(&cold), ["warmup", "ice", "meditate"]);
58e1fb3943e6atagen2026-10-10 13:30 UTC780
    let warm = send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
58e1fb3943e6atagen2026-10-10 13:30 UTC781
    let usable = advertised(&warm);
58e1fb3943e6atagen2026-10-10 13:30 UTC782
    assert!(usable.contains(&"wet".to_owned()) && usable.contains(&"dry".to_owned()));
58e1fb3943e6atagen2026-10-10 13:30 UTC783
    assert!(!usable.contains(&"uli".to_owned()), "uli needs the forum");
58e1fb3943e6atagen2026-10-10 13:30 UTC784
    assert!(!usable.contains(&"pump".to_owned()), "pump needs a pump");
58e1fb3943e6atagen2026-10-10 13:30 UTC785
    assert!(
58e1fb3943e6atagen2026-10-10 13:30 UTC786
        header(&warm, "link").contains("title=\"wet\"; allow=\"JELQ\"; lubricant=\"required\"")
58e1fb3943e6atagen2026-10-10 13:30 UTC787
    );
58e1fb3943e6atagen2026-10-10 13:30 UTC788
58e1fb3943e6atagen2026-10-10 13:30 UTC789
    let (forum, _) = seeded(Floor::Forum);
58e1fb3943e6atagen2026-10-10 13:30 UTC790
    let response = send(&forum, "GET", "/runs/cafebabe", &[], "").await;
58e1fb3943e6atagen2026-10-10 13:30 UTC791
    assert!(advertised(&response).is_empty());
58e1fb3943e6atagen2026-10-10 13:30 UTC792
}
d604371299fbatagen2026-10-10 13:54 UTC793
d604371299fbatagen2026-10-10 13:54 UTC794
fn linked_rels(response: &Response) -> Vec<String> {
d604371299fbatagen2026-10-10 13:54 UTC795
    header(response, "link")
d604371299fbatagen2026-10-10 13:54 UTC796
        .split(", <")
d604371299fbatagen2026-10-10 13:54 UTC797
        .filter_map(|entry| {
d604371299fbatagen2026-10-10 13:54 UTC798
            entry
d604371299fbatagen2026-10-10 13:54 UTC799
                .split("rels/")
d604371299fbatagen2026-10-10 13:54 UTC800
                .nth(1)?
d604371299fbatagen2026-10-10 13:54 UTC801
                .split('"')
d604371299fbatagen2026-10-10 13:54 UTC802
                .next()
d604371299fbatagen2026-10-10 13:54 UTC803
                .map(str::to_owned)
d604371299fbatagen2026-10-10 13:54 UTC804
        })
d604371299fbatagen2026-10-10 13:54 UTC805
        .filter(|rel| rel != "technique")
d604371299fbatagen2026-10-10 13:54 UTC806
        .collect()
d604371299fbatagen2026-10-10 13:54 UTC807
}
d604371299fbatagen2026-10-10 13:54 UTC808
d604371299fbatagen2026-10-10 13:54 UTC809
#[tokio::test]
d604371299fbatagen2026-10-10 13:54 UTC810
async fn links_hide_resources_that_would_do_nothing() {
d604371299fbatagen2026-10-10 13:54 UTC811
    let (bedroom, _) = seeded(Floor::Bedroom);
d604371299fbatagen2026-10-10 13:54 UTC812
    let before = linked_rels(&send(&bedroom, "GET", "/runs/cafebabe", &[], "").await);
d604371299fbatagen2026-10-10 13:54 UTC813
    assert!(before.contains(&"drawer".to_owned()));
d604371299fbatagen2026-10-10 13:54 UTC814
    assert!(!before.contains(&"diagnosis".to_owned()));
d604371299fbatagen2026-10-10 13:54 UTC815
    assert!(!before.contains(&"thread".to_owned()));
d604371299fbatagen2026-10-10 13:54 UTC816
    let searched = send(&bedroom, "POST", "/runs/cafebabe/drawer", &[], "").await;
d604371299fbatagen2026-10-10 13:54 UTC817
    assert!(!linked_rels(&searched).contains(&"drawer".to_owned()));
d604371299fbatagen2026-10-10 13:54 UTC818
d604371299fbatagen2026-10-10 13:54 UTC819
    let (clinic, _) = seeded(Floor::Clinic);
d604371299fbatagen2026-10-10 13:54 UTC820
    let rels = linked_rels(&send(&clinic, "GET", "/runs/cafebabe", &[], "").await);
d604371299fbatagen2026-10-10 13:54 UTC821
    assert!(rels.contains(&"diagnosis".to_owned()) && rels.contains(&"thread".to_owned()));
d604371299fbatagen2026-10-10 13:54 UTC822
}
0d604ee39751atagen2026-10-10 14:15 UTC823
0d604ee39751atagen2026-10-10 14:15 UTC824
#[tokio::test]
0d604ee39751atagen2026-10-10 14:15 UTC825
async fn ignoring_mum_has_consequences() {
0d604ee39751atagen2026-10-10 14:15 UTC826
    let app = seeded_with(Floor::Bedroom, |run| {
0d604ee39751atagen2026-10-10 14:15 UTC827
        mum_at_the_door(run);
0d604ee39751atagen2026-10-10 14:15 UTC828
        run.here.turns = 6;
0d604ee39751atagen2026-10-10 14:15 UTC829
    });
0d604ee39751atagen2026-10-10 14:15 UTC830
    let knock = send(&app, "GET", "/runs/cafebabe", &[], "").await;
0d604ee39751atagen2026-10-10 14:15 UTC831
    assert_eq!(
0d604ee39751atagen2026-10-10 14:15 UTC832
        json(knock).await["type"],
0d604ee39751atagen2026-10-10 14:15 UTC833
        "https://jelq.dev/problems/someone-at-the-door"
0d604ee39751atagen2026-10-10 14:15 UTC834
    );
0d604ee39751atagen2026-10-10 14:15 UTC835
    let leave = send(&app, "POST", "/runs/cafebabe/floor", &[], "").await;
0d604ee39751atagen2026-10-10 14:15 UTC836
    assert_eq!(
0d604ee39751atagen2026-10-10 14:15 UTC837
        leave.status(),
0d604ee39751atagen2026-10-10 14:15 UTC838
        StatusCode::UNAUTHORIZED,
0d604ee39751atagen2026-10-10 14:15 UTC839
        "no leaving with Mum at the door"
0d604ee39751atagen2026-10-10 14:15 UTC840
    );
0d604ee39751atagen2026-10-10 14:15 UTC841
    send(&app, "GET", "/runs/cafebabe", &[], "").await;
0d604ee39751atagen2026-10-10 14:15 UTC842
    let opened = send(&app, "GET", "/runs/cafebabe", &[], "").await;
0d604ee39751atagen2026-10-10 14:15 UTC843
    assert_eq!(opened.status(), StatusCode::GONE);
0d604ee39751atagen2026-10-10 14:15 UTC844
    assert_eq!(
0d604ee39751atagen2026-10-10 14:15 UTC845
        json(opened).await["type"],
0d604ee39751atagen2026-10-10 14:15 UTC846
        "https://jelq.dev/problems/opened"
0d604ee39751atagen2026-10-10 14:15 UTC847
    );
0d604ee39751atagen2026-10-10 14:15 UTC848
}
0d604ee39751atagen2026-10-10 14:15 UTC849
0d604ee39751atagen2026-10-10 14:15 UTC850
#[tokio::test]
0d604ee39751atagen2026-10-10 14:15 UTC851
async fn caged_drawers_are_noisy_and_eventually_give_up_the_key() {
0d604ee39751atagen2026-10-10 14:15 UTC852
    let app = seeded_with(Floor::ChangingRoom, |run| run.items.cage = true);
0d604ee39751atagen2026-10-10 14:15 UTC853
    let mut statuses = Vec::new();
0d604ee39751atagen2026-10-10 14:15 UTC854
    for _ in 0..12 {
0d604ee39751atagen2026-10-10 14:15 UTC855
        let response = send(&app, "POST", "/runs/cafebabe/drawer", &[], "").await;
0d604ee39751atagen2026-10-10 14:15 UTC856
        statuses.push(response.status());
0d604ee39751atagen2026-10-10 14:15 UTC857
        if response.status() == StatusCode::OK {
0d604ee39751atagen2026-10-10 14:15 UTC858
            break;
0d604ee39751atagen2026-10-10 14:15 UTC859
        }
0d604ee39751atagen2026-10-10 14:15 UTC860
    }
0d604ee39751atagen2026-10-10 14:15 UTC861
    assert_eq!(statuses.last(), Some(&StatusCode::OK), "{statuses:?}");
0d604ee39751atagen2026-10-10 14:15 UTC862
    assert!(
0d604ee39751atagen2026-10-10 14:15 UTC863
        statuses[..statuses.len() - 1]
0d604ee39751atagen2026-10-10 14:15 UTC864
            .iter()
0d604ee39751atagen2026-10-10 14:15 UTC865
            .all(|s| *s == StatusCode::NO_CONTENT)
0d604ee39751atagen2026-10-10 14:15 UTC866
    );
0d604ee39751atagen2026-10-10 14:15 UTC867
}
f39b10e20a0batagen2026-10-10 14:33 UTC868
f39b10e20a0batagen2026-10-10 14:33 UTC869
#[tokio::test]
f39b10e20a0batagen2026-10-10 14:33 UTC870
async fn the_ruler_replaces_the_self_report() {
f39b10e20a0batagen2026-10-10 14:33 UTC871
    let app = seeded_with(Floor::Bedroom, |_| {});
f39b10e20a0batagen2026-10-10 14:33 UTC872
    let before = send(&app, "GET", "/runs/cafebabe", &[], "").await;
f39b10e20a0batagen2026-10-10 14:33 UTC873
    assert!(before.headers().contains_key("x-self-reported-length"));
f39b10e20a0batagen2026-10-10 14:33 UTC874
    assert!(!before.headers().contains_key("x-length"));
f39b10e20a0batagen2026-10-10 14:33 UTC875
f39b10e20a0batagen2026-10-10 14:33 UTC876
    let app = seeded_with(Floor::Bedroom, |run| run.items.ruler = true);
f39b10e20a0batagen2026-10-10 14:33 UTC877
    let after = send(&app, "GET", "/runs/cafebabe", &[], "").await;
f39b10e20a0batagen2026-10-10 14:33 UTC878
    assert!(!after.headers().contains_key("x-self-reported-length"));
f39b10e20a0batagen2026-10-10 14:33 UTC879
    assert!(after.headers().contains_key("x-length"));
f39b10e20a0batagen2026-10-10 14:33 UTC880
}
d46f9d0b953aatagen2026-10-10 14:41 UTC881
d46f9d0b953aatagen2026-10-10 14:41 UTC882
#[tokio::test]
d46f9d0b953aatagen2026-10-10 14:41 UTC883
async fn an_open_tab_is_linked_and_jelq_still_answers_with_choices() {
d46f9d0b953aatagen2026-10-10 14:41 UTC884
    let app = seeded_with(Floor::Bathroom, |run| {
d46f9d0b953aatagen2026-10-10 14:41 UTC885
        run.story = Story::Pending;
d46f9d0b953aatagen2026-10-10 14:41 UTC886
        run.warmed_up = true;
d46f9d0b953aatagen2026-10-10 14:41 UTC887
    });
d46f9d0b953aatagen2026-10-10 14:41 UTC888
    let response = send(&app, "GET", "/runs/cafebabe", &[], "").await;
d46f9d0b953aatagen2026-10-10 14:41 UTC889
    let links = header(&response, "link");
d46f9d0b953aatagen2026-10-10 14:41 UTC890
    assert!(links.contains("/branches/forum>; rel=\"https://jelq.dev/rels/branch\""));
d46f9d0b953aatagen2026-10-10 14:41 UTC891
    assert!(links.contains("/branches/close-tab>; rel=\"https://jelq.dev/rels/branch\""));
d46f9d0b953aatagen2026-10-10 14:41 UTC892
    assert!(links.contains("?technique=wet>"));
d46f9d0b953aatagen2026-10-10 14:41 UTC893
    let jelq = send(&app, "JELQ", "/runs/cafebabe?technique=edge", &[], "").await;
d46f9d0b953aatagen2026-10-10 14:41 UTC894
    assert_eq!(jelq.status(), StatusCode::MULTIPLE_CHOICES);
d46f9d0b953aatagen2026-10-10 14:41 UTC895
}
d46f9d0b953aatagen2026-10-10 14:41 UTC896
d46f9d0b953aatagen2026-10-10 14:41 UTC897
#[tokio::test]
d46f9d0b953aatagen2026-10-10 14:41 UTC898
async fn either_story_choice_is_reachable_during_cooldown() {
d46f9d0b953aatagen2026-10-10 14:41 UTC899
    for choice in ["forum", "close-tab"] {
d46f9d0b953aatagen2026-10-10 14:41 UTC900
        let app = seeded_with(Floor::Bathroom, |run| {
d46f9d0b953aatagen2026-10-10 14:41 UTC901
            run.turn = 9;
d46f9d0b953aatagen2026-10-10 14:41 UTC902
            run.warmed_up = true;
e807266c323batagen2026-10-12 09:46 UTC903
            run.eq = 45;
d46f9d0b953aatagen2026-10-10 14:41 UTC904
            run.stamina = 1;
d46f9d0b953aatagen2026-10-10 14:41 UTC905
            run.items.door_locks = 1;
d46f9d0b953aatagen2026-10-10 14:41 UTC906
        });
d46f9d0b953aatagen2026-10-10 14:41 UTC907
        let tenth = send(&app, "JELQ", "/runs/cafebabe?technique=edge", &[], "").await;
d46f9d0b953aatagen2026-10-10 14:41 UTC908
        assert_eq!(tenth.status(), StatusCode::OK);
d46f9d0b953aatagen2026-10-10 14:41 UTC909
        let response = send(&app, "GET", "/runs/cafebabe", &[], "").await;
d46f9d0b953aatagen2026-10-10 14:41 UTC910
        assert!(!header(&response, "link").contains("?technique="));
d46f9d0b953aatagen2026-10-10 14:41 UTC911
        let path = format!("/runs/cafebabe/branches/{choice}");
d46f9d0b953aatagen2026-10-10 14:41 UTC912
        assert!(
d46f9d0b953aatagen2026-10-10 14:41 UTC913
            header(&response, "link")
d46f9d0b953aatagen2026-10-10 14:41 UTC914
                .contains(&format!("<{path}>; rel=\"https://jelq.dev/rels/branch\""))
d46f9d0b953aatagen2026-10-10 14:41 UTC915
        );
d46f9d0b953aatagen2026-10-10 14:41 UTC916
        let chosen = send(&app, "POST", &path, &[], "").await;
d46f9d0b953aatagen2026-10-10 14:41 UTC917
        assert_eq!(chosen.status(), StatusCode::SEE_OTHER);
d46f9d0b953aatagen2026-10-10 14:41 UTC918
        assert!(!header(&chosen, "link").contains("/branches/"));
d46f9d0b953aatagen2026-10-10 14:41 UTC919
        let repeated = send(&app, "POST", &path, &[], "").await;
d46f9d0b953aatagen2026-10-10 14:41 UTC920
        assert_eq!(repeated.status(), StatusCode::CONFLICT);
d46f9d0b953aatagen2026-10-10 14:41 UTC921
    }
d46f9d0b953aatagen2026-10-10 14:41 UTC922
}
d46f9d0b953aatagen2026-10-10 14:41 UTC923
d46f9d0b953aatagen2026-10-10 14:41 UTC924
#[tokio::test]
d46f9d0b953aatagen2026-10-10 14:41 UTC925
async fn a_door_lock_says_when_it_was_used() {
d46f9d0b953aatagen2026-10-10 14:41 UTC926
    let app = seeded_with(Floor::Bathroom, |run| {
d46f9d0b953aatagen2026-10-10 14:41 UTC927
        run.items.door_locks = 1;
d46f9d0b953aatagen2026-10-10 14:41 UTC928
        run.suspicion = SUSPICION_LIMIT;
d46f9d0b953aatagen2026-10-10 14:41 UTC929
        run.warmed_up = true;
d46f9d0b953aatagen2026-10-10 14:41 UTC930
    });
d46f9d0b953aatagen2026-10-10 14:41 UTC931
    let response = send(&app, "JELQ", "/runs/cafebabe?technique=dry", &[], "").await;
d46f9d0b953aatagen2026-10-10 14:41 UTC932
    assert_ne!(response.status(), StatusCode::CONFLICT);
d46f9d0b953aatagen2026-10-10 14:41 UTC933
    assert!(
d46f9d0b953aatagen2026-10-10 14:41 UTC934
        response
d46f9d0b953aatagen2026-10-10 14:41 UTC935
            .headers()
d46f9d0b953aatagen2026-10-10 14:41 UTC936
            .get_all("warning")
d46f9d0b953aatagen2026-10-10 14:41 UTC937
            .iter()
7d3fb63208bfatagen2026-10-11 04:38 UTC938
            .any(|w| w.to_str().unwrap().contains("your brother tried the door"))
d46f9d0b953aatagen2026-10-10 14:41 UTC939
    );
d46f9d0b953aatagen2026-10-10 14:41 UTC940
}
7d3fb63208bfatagen2026-10-11 04:38 UTC941
7d3fb63208bfatagen2026-10-11 04:38 UTC942
#[tokio::test]
7d3fb63208bfatagen2026-10-11 04:38 UTC943
async fn shopping_survives_browsing_but_not_jelqing() {
7d3fb63208bfatagen2026-10-11 04:38 UTC944
    let app = seeded_with(Floor::ChangingRoom, |run| run.cash = 50);
7d3fb63208bfatagen2026-10-11 04:38 UTC945
    send(&app, "GET", "/runs/cafebabe/shop", &[], "").await;
7d3fb63208bfatagen2026-10-11 04:38 UTC946
    let view = send(&app, "GET", "/runs/cafebabe", &[], "").await;
7d3fb63208bfatagen2026-10-11 04:38 UTC947
    assert!(header(&view, "link").contains("shop-item"));
7d3fb63208bfatagen2026-10-11 04:38 UTC948
    let jelq = send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
7d3fb63208bfatagen2026-10-11 04:38 UTC949
    assert_ne!(jelq.status(), StatusCode::METHOD_NOT_ALLOWED);
7d3fb63208bfatagen2026-10-11 04:38 UTC950
    let after = send(&app, "GET", "/runs/cafebabe", &[], "").await;
7d3fb63208bfatagen2026-10-11 04:38 UTC951
    assert!(!header(&after, "link").contains("shop-item"));
7d3fb63208bfatagen2026-10-11 04:38 UTC952
}
0fdc47364c8catagen2026-10-11 04:45 UTC953
0fdc47364c8catagen2026-10-11 04:45 UTC954
#[tokio::test]
0fdc47364c8catagen2026-10-11 04:45 UTC955
async fn the_thread_announces_itself_at_the_branch() {
0fdc47364c8catagen2026-10-11 04:45 UTC956
    let app = seeded_with(Floor::Bathroom, |run| {
0fdc47364c8catagen2026-10-11 04:45 UTC957
        run.turn = 8;
0fdc47364c8catagen2026-10-11 04:45 UTC958
        run.warmed_up = true;
0a0bfee8329eatagen2026-10-11 05:11 UTC959
        run.items.baby_oil = 9;
0fdc47364c8catagen2026-10-11 04:45 UTC960
    });
0fdc47364c8catagen2026-10-11 04:45 UTC961
    send(
0fdc47364c8catagen2026-10-11 04:45 UTC962
        &app,
0fdc47364c8catagen2026-10-11 04:45 UTC963
        "JELQ",
0fdc47364c8catagen2026-10-11 04:45 UTC964
        "/runs/cafebabe?technique=wet",
0fdc47364c8catagen2026-10-11 04:45 UTC965
        &[("content-type", "application/baby-oil")],
0fdc47364c8catagen2026-10-11 04:45 UTC966
        "",
0fdc47364c8catagen2026-10-11 04:45 UTC967
    )
0fdc47364c8catagen2026-10-11 04:45 UTC968
    .await;
0fdc47364c8catagen2026-10-11 04:45 UTC969
    let ninth = send(&app, "GET", "/runs/cafebabe", &[], "").await;
0fdc47364c8catagen2026-10-11 04:45 UTC970
    assert!(!header(&ninth, "link").contains("/forum/threads/2009"));
0fdc47364c8catagen2026-10-11 04:45 UTC971
    let tenth = send(
0fdc47364c8catagen2026-10-11 04:45 UTC972
        &app,
0fdc47364c8catagen2026-10-11 04:45 UTC973
        "JELQ",
0fdc47364c8catagen2026-10-11 04:45 UTC974
        "/runs/cafebabe?technique=wet",
0fdc47364c8catagen2026-10-11 04:45 UTC975
        &[("content-type", "application/baby-oil")],
0fdc47364c8catagen2026-10-11 04:45 UTC976
        "",
0fdc47364c8catagen2026-10-11 04:45 UTC977
    )
0fdc47364c8catagen2026-10-11 04:45 UTC978
    .await;
0fdc47364c8catagen2026-10-11 04:45 UTC979
    assert!(
0fdc47364c8catagen2026-10-11 04:45 UTC980
        tenth
0fdc47364c8catagen2026-10-11 04:45 UTC981
            .headers()
0fdc47364c8catagen2026-10-11 04:45 UTC982
            .get_all("warning")
0fdc47364c8catagen2026-10-11 04:45 UTC983
            .iter()
0fdc47364c8catagen2026-10-11 04:45 UTC984
            .any(|w| w.to_str().unwrap().contains("a forum thread from 2009"))
0fdc47364c8catagen2026-10-11 04:45 UTC985
    );
0fdc47364c8catagen2026-10-11 04:45 UTC986
    assert!(header(&tenth, "link").contains("/forum/threads/2009"));
0fdc47364c8catagen2026-10-11 04:45 UTC987
    let announced = send(&app, "GET", "/runs/cafebabe", &[], "").await;
0fdc47364c8catagen2026-10-11 04:45 UTC988
    assert!(header(&announced, "link").contains("/forum/threads/2009"));
0fdc47364c8catagen2026-10-11 04:45 UTC989
}
0fdc47364c8catagen2026-10-11 04:45 UTC990
0fdc47364c8catagen2026-10-11 04:45 UTC991
#[tokio::test]
b85a0d4536c3atagen2026-10-11 08:50 UTC992
async fn rereading_the_thread_teaches_uli() {
b85a0d4536c3atagen2026-10-11 08:50 UTC993
    let mut taught_runs = 0;
b85a0d4536c3atagen2026-10-11 08:50 UTC994
    for seed in 0..100u64 {
b85a0d4536c3atagen2026-10-11 08:50 UTC995
        let app = app_with(Floor::Bedroom, seed, |run| {
e807266c323batagen2026-10-12 09:46 UTC996
            run.turn = BRANCH_TURN;
b85a0d4536c3atagen2026-10-11 08:50 UTC997
            run.warmed_up = true;
b85a0d4536c3atagen2026-10-11 08:50 UTC998
            run.story = Story::ClosedTab;
b85a0d4536c3atagen2026-10-11 08:50 UTC999
        });
b85a0d4536c3atagen2026-10-11 08:50 UTC1000
        let mut taught = false;
a6caa2f99786atagen2026-10-12 10:38 UTC1001
        for _ in 0..2 {
b85a0d4536c3atagen2026-10-11 08:50 UTC1002
            let read = send(&app, "POST", "/runs/cafebabe/re-read", &[], "").await;
b85a0d4536c3atagen2026-10-11 08:50 UTC1003
            match read.status() {
b85a0d4536c3atagen2026-10-11 08:50 UTC1004
                StatusCode::NOT_MODIFIED => {
b85a0d4536c3atagen2026-10-11 08:50 UTC1005
                    assert!(header(&read, "warning").contains("2009"));
b85a0d4536c3atagen2026-10-11 08:50 UTC1006
                }
b85a0d4536c3atagen2026-10-11 08:50 UTC1007
                StatusCode::OK => {
b85a0d4536c3atagen2026-10-11 08:50 UTC1008
                    assert!(header(&read, "location").contains("/techniques"));
b85a0d4536c3atagen2026-10-11 08:50 UTC1009
                    taught = true;
b85a0d4536c3atagen2026-10-11 08:50 UTC1010
                    break;
b85a0d4536c3atagen2026-10-11 08:50 UTC1011
                }
b85a0d4536c3atagen2026-10-11 08:50 UTC1012
                other => panic!("seed {seed} read the thread into {other}"),
b85a0d4536c3atagen2026-10-11 08:50 UTC1013
            }
b85a0d4536c3atagen2026-10-11 08:50 UTC1014
        }
a6caa2f99786atagen2026-10-12 10:38 UTC1015
        let state = send(&app, "GET", "/runs/cafebabe", &[], "").await;
a6caa2f99786atagen2026-10-12 10:38 UTC1016
        assert_eq!(header(&state, "x-stamina"), "10/10", "reading is free");
b85a0d4536c3atagen2026-10-11 08:50 UTC1017
        if taught {
b85a0d4536c3atagen2026-10-11 08:50 UTC1018
            taught_runs += 1;
b85a0d4536c3atagen2026-10-11 08:50 UTC1019
            let jelq = send(&app, "JELQ", "/runs/cafebabe?technique=uli", &[], "").await;
b85a0d4536c3atagen2026-10-11 08:50 UTC1020
            assert_ne!(jelq.status(), StatusCode::FORBIDDEN);
a6caa2f99786atagen2026-10-12 10:38 UTC1021
        } else {
a6caa2f99786atagen2026-10-12 10:38 UTC1022
            let cached = send(&app, "POST", "/runs/cafebabe/re-read", &[], "").await;
a6caa2f99786atagen2026-10-12 10:38 UTC1023
            assert_eq!(cached.status(), StatusCode::OK);
a6caa2f99786atagen2026-10-12 10:38 UTC1024
            assert!(header(&cached, "warning").starts_with("112"));
a6caa2f99786atagen2026-10-12 10:38 UTC1025
            assert!(header(&cached, "age").parse::<u64>().unwrap() > 0);
b85a0d4536c3atagen2026-10-11 08:50 UTC1026
        }
b85a0d4536c3atagen2026-10-11 08:50 UTC1027
    }
0fdc47364c8catagen2026-10-11 04:45 UTC1028
    assert!(
a6caa2f99786atagen2026-10-12 10:38 UTC1029
        (30..=58).contains(&taught_runs),
a6caa2f99786atagen2026-10-12 10:38 UTC1030
        "{taught_runs}/100 learned uli in two reads"
0fdc47364c8catagen2026-10-11 04:45 UTC1031
    );
0fdc47364c8catagen2026-10-11 04:45 UTC1032
}
0fdc47364c8catagen2026-10-11 04:45 UTC1033
0fdc47364c8catagen2026-10-11 04:45 UTC1034
#[tokio::test]
0fdc47364c8catagen2026-10-11 04:45 UTC1035
async fn meditation_is_free_but_only_once_per_floor() {
b85a0d4536c3atagen2026-10-11 08:50 UTC1036
    let app = seeded_with(Floor::Bedroom, |_| {});
0fdc47364c8catagen2026-10-11 04:45 UTC1037
    let response = send(&app, "JELQ", "/runs/cafebabe?technique=meditate", &[], "").await;
0fdc47364c8catagen2026-10-11 04:45 UTC1038
    assert_eq!(response.status(), StatusCode::OK);
0fdc47364c8catagen2026-10-11 04:45 UTC1039
    assert_eq!(header(&response, "x-eq"), "10");
0fdc47364c8catagen2026-10-11 04:45 UTC1040
    assert_eq!(header(&response, "x-stamina"), "10/10");
0fdc47364c8catagen2026-10-11 04:45 UTC1041
    let again = send(&app, "JELQ", "/runs/cafebabe?technique=meditate", &[], "").await;
0fdc47364c8catagen2026-10-11 04:45 UTC1042
    assert_eq!(again.status(), StatusCode::CONFLICT);
0fdc47364c8catagen2026-10-11 04:45 UTC1043
}
0fdc47364c8catagen2026-10-11 04:45 UTC1044
0fdc47364c8catagen2026-10-11 04:45 UTC1045
#[tokio::test]
b85a0d4536c3atagen2026-10-11 08:50 UTC1046
async fn meditation_is_listed_from_the_start() {
0fdc47364c8catagen2026-10-11 04:45 UTC1047
    let app = seeded_with(Floor::Bedroom, |_| {});
0fdc47364c8catagen2026-10-11 04:45 UTC1048
    let view = send(&app, "GET", "/runs/cafebabe", &[], "").await;
b85a0d4536c3atagen2026-10-11 08:50 UTC1049
    assert!(header(&view, "link").contains("technique=meditate"));
0fdc47364c8catagen2026-10-11 04:45 UTC1050
    let jelq = send(&app, "JELQ", "/runs/cafebabe?technique=meditate", &[], "").await;
b85a0d4536c3atagen2026-10-11 08:50 UTC1051
    assert_eq!(jelq.status(), StatusCode::OK);
0fdc47364c8catagen2026-10-11 04:45 UTC1052
}
4347977b8192atagen2026-10-11 04:56 UTC1053
4347977b8192atagen2026-10-11 04:56 UTC1054
fn app_with(floor: Floor, seed: u64, setup: impl FnOnce(&mut Run)) -> Router {
4347977b8192atagen2026-10-11 04:56 UTC1055
    let state = Arc::new(AppState::default());
4347977b8192atagen2026-10-11 04:56 UTC1056
    let mut run = Run::new("cafebabe".into(), "cafe".into(), seed);
4347977b8192atagen2026-10-11 04:56 UTC1057
    run.floor = floor;
4347977b8192atagen2026-10-11 04:56 UTC1058
    setup(&mut run);
4347977b8192atagen2026-10-11 04:56 UTC1059
    state.insert(run);
4347977b8192atagen2026-10-11 04:56 UTC1060
    router(state)
4347977b8192atagen2026-10-11 04:56 UTC1061
}
4347977b8192atagen2026-10-11 04:56 UTC1062
4347977b8192atagen2026-10-11 04:56 UTC1063
#[tokio::test]
4347977b8192atagen2026-10-11 04:56 UTC1064
async fn hold_is_offered_only_at_the_edges() {
4347977b8192atagen2026-10-11 04:56 UTC1065
    let calm = seeded_with(Floor::Bedroom, |_| {});
4347977b8192atagen2026-10-11 04:56 UTC1066
    let refused = send(&calm, "HOLD", "/runs/cafebabe", &[], "").await;
4347977b8192atagen2026-10-11 04:56 UTC1067
    assert_eq!(refused.status(), StatusCode::METHOD_NOT_ALLOWED);
4347977b8192atagen2026-10-11 04:56 UTC1068
    assert!(!header(&refused, "allow").contains("HOLD"));
4347977b8192atagen2026-10-11 04:56 UTC1069
    let view = send(&calm, "GET", "/runs/cafebabe", &[], "").await;
4347977b8192atagen2026-10-11 04:56 UTC1070
    assert!(!header(&view, "link").contains("HOLD"));
4347977b8192atagen2026-10-11 04:56 UTC1071
    let options = send(&calm, "OPTIONS", "/runs/cafebabe", &[], "").await;
4347977b8192atagen2026-10-11 04:56 UTC1072
    assert!(!header(&options, "allow").contains("HOLD"));
4347977b8192atagen2026-10-11 04:56 UTC1073
4347977b8192atagen2026-10-11 04:56 UTC1074
    let winded = seeded_with(Floor::Bedroom, |run| run.stamina = 2);
4347977b8192atagen2026-10-11 04:56 UTC1075
    let listed = send(&winded, "GET", "/runs/cafebabe", &[], "").await;
4347977b8192atagen2026-10-11 04:56 UTC1076
    assert!(header(&listed, "link").contains("HOLD"));
4347977b8192atagen2026-10-11 04:56 UTC1077
    let options = send(&winded, "OPTIONS", "/runs/cafebabe", &[], "").await;
4347977b8192atagen2026-10-11 04:56 UTC1078
    assert!(header(&options, "allow").contains("HOLD"));
4347977b8192atagen2026-10-11 04:56 UTC1079
4347977b8192atagen2026-10-11 04:56 UTC1080
    let hot = seeded_with(Floor::Bedroom, |run| {
4347977b8192atagen2026-10-11 04:56 UTC1081
        run.eq = 75;
4347977b8192atagen2026-10-11 04:56 UTC1082
        run.warmed_up = true;
4347977b8192atagen2026-10-11 04:56 UTC1083
    });
4347977b8192atagen2026-10-11 04:56 UTC1084
    let held = send(&hot, "HOLD", "/runs/cafebabe", &[], "").await;
4347977b8192atagen2026-10-11 04:56 UTC1085
    assert_eq!(held.status(), StatusCode::NO_CONTENT);
4347977b8192atagen2026-10-11 04:56 UTC1086
    assert_eq!(header(&held, "x-stamina"), "10/10");
4347977b8192atagen2026-10-11 04:56 UTC1087
}
4347977b8192atagen2026-10-11 04:56 UTC1088
4347977b8192atagen2026-10-11 04:56 UTC1089
#[tokio::test]
4347977b8192atagen2026-10-11 04:56 UTC1090
async fn holding_calms_and_sometimes_reflects() {
b85a0d4536c3atagen2026-10-11 08:50 UTC1091
    let mut calmed = 0;
b85a0d4536c3atagen2026-10-11 08:50 UTC1092
    let mut reflected = 0;
b85a0d4536c3atagen2026-10-11 08:50 UTC1093
    let mut ruptured = 0;
b85a0d4536c3atagen2026-10-11 08:50 UTC1094
    for seed in 0..120u64 {
b85a0d4536c3atagen2026-10-11 08:50 UTC1095
        let mut run = Run::new("cafebabe".into(), "cafe".into(), seed);
b85a0d4536c3atagen2026-10-11 08:50 UTC1096
        run.eq = 75;
a6caa2f99786atagen2026-10-12 10:38 UTC1097
        match run.hold() {
b85a0d4536c3atagen2026-10-11 08:50 UTC1098
            Ok(_) if run.eq == 0 => reflected += 1,
b85a0d4536c3atagen2026-10-11 08:50 UTC1099
            Ok(_) => calmed += 1,
b85a0d4536c3atagen2026-10-11 08:50 UTC1100
            Err(_) => ruptured += 1,
b85a0d4536c3atagen2026-10-11 08:50 UTC1101
        }
b85a0d4536c3atagen2026-10-11 08:50 UTC1102
    }
4347977b8192atagen2026-10-11 04:56 UTC1103
    assert!(
b85a0d4536c3atagen2026-10-11 08:50 UTC1104
        calmed > 100 && reflected > 0,
b85a0d4536c3atagen2026-10-11 08:50 UTC1105
        "one HOLD in twelve should reflect, got {calmed} calm and {reflected} reflective of 120"
b85a0d4536c3atagen2026-10-11 08:50 UTC1106
    );
b85a0d4536c3atagen2026-10-11 08:50 UTC1107
    assert!(
b85a0d4536c3atagen2026-10-11 08:50 UTC1108
        calmed + reflected + ruptured == 120 && ruptured <= 4,
b85a0d4536c3atagen2026-10-11 08:50 UTC1109
        "the flat 1 in 250 ruptured {ruptured} of 120 holds"
4347977b8192atagen2026-10-11 04:56 UTC1110
    );
4347977b8192atagen2026-10-11 04:56 UTC1111
}
4347977b8192atagen2026-10-11 04:56 UTC1112
4347977b8192atagen2026-10-11 04:56 UTC1113
#[tokio::test]
4347977b8192atagen2026-10-11 04:56 UTC1114
async fn leaving_the_band_warms_the_next_floor() {
4347977b8192atagen2026-10-11 04:56 UTC1115
    let app = seeded_with(Floor::Bedroom, |run| {
4347977b8192atagen2026-10-11 04:56 UTC1116
        run.eq = 60;
4347977b8192atagen2026-10-11 04:56 UTC1117
        run.warmed_up = true;
4347977b8192atagen2026-10-11 04:56 UTC1118
        run.here.turns = 6;
0a0bfee8329eatagen2026-10-11 05:11 UTC1119
        run.items.baby_oil = 9;
4347977b8192atagen2026-10-11 04:56 UTC1120
    });
4347977b8192atagen2026-10-11 04:56 UTC1121
    let left = send(&app, "POST", "/runs/cafebabe/floor", &[], "").await;
4347977b8192atagen2026-10-11 04:56 UTC1122
    assert_eq!(left.status(), StatusCode::SEE_OTHER);
4347977b8192atagen2026-10-11 04:56 UTC1123
    let detail = json(left).await["detail"].as_str().unwrap().to_owned();
4347977b8192atagen2026-10-11 04:56 UTC1124
    assert!(detail.contains("the warmth came with you"));
4347977b8192atagen2026-10-11 04:56 UTC1125
4347977b8192atagen2026-10-11 04:56 UTC1126
    let lube = [("content-type", "application/baby-oil")];
4347977b8192atagen2026-10-11 04:56 UTC1127
    send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
4347977b8192atagen2026-10-11 04:56 UTC1128
    // The shower is a +35 warm-up, so EQ lands in the band and the band bonus rides along.
4347977b8192atagen2026-10-11 04:56 UTC1129
    // The arrival bonus is the one point the second jelq loses.
4347977b8192atagen2026-10-11 04:56 UTC1130
    let first = send(&app, "JELQ", "/runs/cafebabe?technique=wet", &lube, "").await;
4347977b8192atagen2026-10-11 04:56 UTC1131
    assert!(
4347977b8192atagen2026-10-11 04:56 UTC1132
        header(&first, "server-timing").contains("d20+4"),
4347977b8192atagen2026-10-11 04:56 UTC1133
        "the first jelq of a warm arrival rolls with +1, got {}",
4347977b8192atagen2026-10-11 04:56 UTC1134
        header(&first, "server-timing")
4347977b8192atagen2026-10-11 04:56 UTC1135
    );
4347977b8192atagen2026-10-11 04:56 UTC1136
    let second = send(&app, "JELQ", "/runs/cafebabe?technique=wet", &lube, "").await;
4347977b8192atagen2026-10-11 04:56 UTC1137
    assert!(header(&second, "server-timing").contains("d20+3"));
4347977b8192atagen2026-10-11 04:56 UTC1138
}
4347977b8192atagen2026-10-11 04:56 UTC1139
4347977b8192atagen2026-10-11 04:56 UTC1140
#[tokio::test]
4347977b8192atagen2026-10-11 04:56 UTC1141
async fn greed_multiplies_gains_above_the_band() {
4347977b8192atagen2026-10-11 04:56 UTC1142
    let lube = [("content-type", "application/baby-oil")];
4347977b8192atagen2026-10-11 04:56 UTC1143
    for seed in 0..60u64 {
4347977b8192atagen2026-10-11 04:56 UTC1144
        let app = app_with(Floor::Bedroom, seed, |run| {
4347977b8192atagen2026-10-11 04:56 UTC1145
            run.eq = 60;
4347977b8192atagen2026-10-11 04:56 UTC1146
            run.warmed_up = true;
4347977b8192atagen2026-10-11 04:56 UTC1147
        });
4347977b8192atagen2026-10-11 04:56 UTC1148
        let in_band = send(&app, "JELQ", "/runs/cafebabe?technique=wet", &lube, "").await;
4347977b8192atagen2026-10-11 04:56 UTC1149
        let app = app_with(Floor::Bedroom, seed, |run| {
4347977b8192atagen2026-10-11 04:56 UTC1150
            run.eq = 75;
4347977b8192atagen2026-10-11 04:56 UTC1151
            run.warmed_up = true;
4347977b8192atagen2026-10-11 04:56 UTC1152
        });
4347977b8192atagen2026-10-11 04:56 UTC1153
        let greedy = send(&app, "JELQ", "/runs/cafebabe?technique=wet", &lube, "").await;
4347977b8192atagen2026-10-11 04:56 UTC1154
        if in_band.status() != StatusCode::OK || greedy.status() != StatusCode::OK {
4347977b8192atagen2026-10-11 04:56 UTC1155
            continue;
4347977b8192atagen2026-10-11 04:56 UTC1156
        }
4347977b8192atagen2026-10-11 04:56 UTC1157
        let steady = json(in_band).await["length"].as_f64().unwrap();
4347977b8192atagen2026-10-11 04:56 UTC1158
        let eager = json(greedy).await["length"].as_f64().unwrap();
4347977b8192atagen2026-10-11 04:56 UTC1159
        assert!(
4347977b8192atagen2026-10-11 04:56 UTC1160
            (eager - steady * 1.5).abs() < 1e-9,
4347977b8192atagen2026-10-11 04:56 UTC1161
            "seed {seed} gained {eager} above the band against {steady} inside it"
4347977b8192atagen2026-10-11 04:56 UTC1162
        );
4347977b8192atagen2026-10-11 04:56 UTC1163
        return;
4347977b8192atagen2026-10-11 04:56 UTC1164
    }
4347977b8192atagen2026-10-11 04:56 UTC1165
    panic!("no seed in 60 produced two clean rolls to compare");
4347977b8192atagen2026-10-11 04:56 UTC1166
}
0a0bfee8329eatagen2026-10-11 05:11 UTC1167
0a0bfee8329eatagen2026-10-11 05:11 UTC1168
#[tokio::test]
0a0bfee8329eatagen2026-10-11 05:11 UTC1169
async fn the_kitchen_is_only_on_the_home_floors() {
0a0bfee8329eatagen2026-10-11 05:11 UTC1170
    let absent = seeded_with(Floor::Bathroom, |_| {});
0a0bfee8329eatagen2026-10-11 05:11 UTC1171
    let no_kitchen = send(&absent, "POST", "/runs/cafebabe/kitchen", &[], "").await;
0a0bfee8329eatagen2026-10-11 05:11 UTC1172
    assert_eq!(no_kitchen.status(), StatusCode::NOT_FOUND);
0a0bfee8329eatagen2026-10-11 05:11 UTC1173
    let view = send(&absent, "GET", "/runs/cafebabe", &[], "").await;
0a0bfee8329eatagen2026-10-11 05:11 UTC1174
    assert!(!header(&view, "link").contains("/kitchen"));
0a0bfee8329eatagen2026-10-11 05:11 UTC1175
0a0bfee8329eatagen2026-10-11 05:11 UTC1176
    let app = seeded_with(Floor::Bedroom, |_| {});
0a0bfee8329eatagen2026-10-11 05:11 UTC1177
    let view = send(&app, "GET", "/runs/cafebabe", &[], "").await;
0a0bfee8329eatagen2026-10-11 05:11 UTC1178
    assert!(header(&view, "link").contains("/kitchen"));
0a0bfee8329eatagen2026-10-11 05:11 UTC1179
    let raid = send(&app, "POST", "/runs/cafebabe/kitchen", &[], "").await;
0a0bfee8329eatagen2026-10-11 05:11 UTC1180
    assert_ne!(raid.status(), StatusCode::NOT_FOUND);
0a0bfee8329eatagen2026-10-11 05:11 UTC1181
}
0a0bfee8329eatagen2026-10-11 05:11 UTC1182
0a0bfee8329eatagen2026-10-11 05:11 UTC1183
#[tokio::test]
0a0bfee8329eatagen2026-10-11 05:11 UTC1184
async fn raids_cost_the_turn_and_the_fridge_pays_out_sometimes() {
0a0bfee8329eatagen2026-10-11 05:11 UTC1185
    let mut bounties = std::collections::BTreeSet::new();
0a0bfee8329eatagen2026-10-11 05:11 UTC1186
    let mut told = 0;
0a0bfee8329eatagen2026-10-11 05:11 UTC1187
    for seed in 0..80u64 {
0a0bfee8329eatagen2026-10-11 05:11 UTC1188
        let mut run = Run::new("cafebabe".into(), "cafe".into(), seed);
0a0bfee8329eatagen2026-10-11 05:11 UTC1189
        let Ok(outcome) = run.raid_kitchen() else {
0a0bfee8329eatagen2026-10-11 05:11 UTC1190
            panic!("the first raid should never fail");
0a0bfee8329eatagen2026-10-11 05:11 UTC1191
        };
0a0bfee8329eatagen2026-10-11 05:11 UTC1192
        assert_eq!(run.turn, 1, "a raid costs the floor turn");
0a0bfee8329eatagen2026-10-11 05:11 UTC1193
        assert!(!run.here.melon_offered, "no melon at starting EQ");
0a0bfee8329eatagen2026-10-11 05:11 UTC1194
        if outcome.status == StatusCode::NO_CONTENT {
0a0bfee8329eatagen2026-10-11 05:11 UTC1195
            bounties.insert("empty");
0a0bfee8329eatagen2026-10-11 05:11 UTC1196
        } else {
0a0bfee8329eatagen2026-10-11 05:11 UTC1197
            bounties.insert("loot");
0a0bfee8329eatagen2026-10-11 05:11 UTC1198
        }
0a0bfee8329eatagen2026-10-11 05:11 UTC1199
        told += u32::from(run.kitchen_tellings);
0a0bfee8329eatagen2026-10-11 05:11 UTC1200
    }
0a0bfee8329eatagen2026-10-11 05:11 UTC1201
    assert!(bounties.contains("loot") && bounties.contains("empty"));
0a0bfee8329eatagen2026-10-11 05:11 UTC1202
    assert!(
0a0bfee8329eatagen2026-10-11 05:11 UTC1203
        (0..80).any(|seed| {
0a0bfee8329eatagen2026-10-11 05:11 UTC1204
            let mut run = Run::new("cafebabe".into(), "cafe".into(), seed);
0a0bfee8329eatagen2026-10-11 05:11 UTC1205
            run.raid_kitchen().is_ok() && run.kitchen_tellings == 0
0a0bfee8329eatagen2026-10-11 05:11 UTC1206
        }),
0a0bfee8329eatagen2026-10-11 05:11 UTC1207
        "most raids go unremarked"
0a0bfee8329eatagen2026-10-11 05:11 UTC1208
    );
0a0bfee8329eatagen2026-10-11 05:11 UTC1209
    assert!(told > 0, "someone should have been told across 80 raids");
0a0bfee8329eatagen2026-10-11 05:11 UTC1210
}
0a0bfee8329eatagen2026-10-11 05:11 UTC1211
0a0bfee8329eatagen2026-10-11 05:11 UTC1212
#[tokio::test]
0a0bfee8329eatagen2026-10-11 05:11 UTC1213
async fn the_watermelon_offers_itself_above_ninety() {
0a0bfee8329eatagen2026-10-11 05:11 UTC1214
    let mut offered = 0;
0a0bfee8329eatagen2026-10-11 05:11 UTC1215
    let mut caught = 0;
0a0bfee8329eatagen2026-10-11 05:11 UTC1216
    let mut survived = 0;
0a0bfee8329eatagen2026-10-11 05:11 UTC1217
    for seed in 0..120u64 {
0a0bfee8329eatagen2026-10-11 05:11 UTC1218
        let mut run = Run::new("cafebabe".into(), "cafe".into(), seed);
0a0bfee8329eatagen2026-10-11 05:11 UTC1219
        run.eq = 95;
0a0bfee8329eatagen2026-10-11 05:11 UTC1220
        let Ok(outcome) = run.raid_kitchen() else {
0a0bfee8329eatagen2026-10-11 05:11 UTC1221
            continue;
0a0bfee8329eatagen2026-10-11 05:11 UTC1222
        };
0a0bfee8329eatagen2026-10-11 05:11 UTC1223
        if outcome.status != StatusCode::MULTIPLE_CHOICES {
0a0bfee8329eatagen2026-10-11 05:11 UTC1224
            continue;
0a0bfee8329eatagen2026-10-11 05:11 UTC1225
        }
0a0bfee8329eatagen2026-10-11 05:11 UTC1226
        offered += 1;
0a0bfee8329eatagen2026-10-11 05:11 UTC1227
        let gains = run.length;
0a0bfee8329eatagen2026-10-11 05:11 UTC1228
        let disgrace = run.shame;
0a0bfee8329eatagen2026-10-11 05:11 UTC1229
        if run.commit_melon().is_err() {
0a0bfee8329eatagen2026-10-11 05:11 UTC1230
            caught += 1;
0a0bfee8329eatagen2026-10-11 05:11 UTC1231
            assert_eq!(run.ended.as_ref().unwrap().cause.slug(), "watermelon");
0a0bfee8329eatagen2026-10-11 05:11 UTC1232
        } else {
0a0bfee8329eatagen2026-10-11 05:11 UTC1233
            survived += 1;
0a0bfee8329eatagen2026-10-11 05:11 UTC1234
            assert_eq!(run.eq, 0);
0a0bfee8329eatagen2026-10-11 05:11 UTC1235
            assert_eq!(run.shame, disgrace + 5);
0a0bfee8329eatagen2026-10-11 05:11 UTC1236
            assert_eq!(run.length, gains, "the gains are kept");
0a0bfee8329eatagen2026-10-11 05:11 UTC1237
        }
0a0bfee8329eatagen2026-10-11 05:11 UTC1238
    }
0a0bfee8329eatagen2026-10-11 05:11 UTC1239
    assert!(
0a0bfee8329eatagen2026-10-11 05:11 UTC1240
        offered > 20,
0a0bfee8329eatagen2026-10-11 05:11 UTC1241
        "the melon should appear in two fifths of raids"
0a0bfee8329eatagen2026-10-11 05:11 UTC1242
    );
0a0bfee8329eatagen2026-10-11 05:11 UTC1243
    assert!(caught > 0 && survived > 0, "caught is one in three");
0a0bfee8329eatagen2026-10-11 05:11 UTC1244
}
0a0bfee8329eatagen2026-10-11 05:11 UTC1245
0a0bfee8329eatagen2026-10-11 05:11 UTC1246
#[tokio::test]
5cc6ab966f30atagen2026-10-11 11:23 UTC1247
async fn a_melon_offer_does_not_survive_a_non_kitchen_turn() {
5cc6ab966f30atagen2026-10-11 11:23 UTC1248
    for seed in 0..60u64 {
5cc6ab966f30atagen2026-10-11 11:23 UTC1249
        let jelq_app = app_with(Floor::Bedroom, seed, |run| {
5cc6ab966f30atagen2026-10-11 11:23 UTC1250
            run.warmed_up = true;
5cc6ab966f30atagen2026-10-11 11:23 UTC1251
            run.here.melon_offered = true;
5cc6ab966f30atagen2026-10-11 11:23 UTC1252
        });
5cc6ab966f30atagen2026-10-11 11:23 UTC1253
        let stroked = send(
5cc6ab966f30atagen2026-10-11 11:23 UTC1254
            &jelq_app,
5cc6ab966f30atagen2026-10-11 11:23 UTC1255
            "JELQ",
5cc6ab966f30atagen2026-10-11 11:23 UTC1256
            "/runs/cafebabe?technique=wet&intensity=2",
5cc6ab966f30atagen2026-10-11 11:23 UTC1257
            &[],
5cc6ab966f30atagen2026-10-11 11:23 UTC1258
            "",
5cc6ab966f30atagen2026-10-11 11:23 UTC1259
        )
5cc6ab966f30atagen2026-10-11 11:23 UTC1260
        .await;
5cc6ab966f30atagen2026-10-11 11:23 UTC1261
        if stroked.status() != StatusCode::OK && stroked.status() != StatusCode::PARTIAL_CONTENT {
5cc6ab966f30atagen2026-10-11 11:23 UTC1262
            continue;
5cc6ab966f30atagen2026-10-11 11:23 UTC1263
        }
5cc6ab966f30atagen2026-10-11 11:23 UTC1264
        let bite = send(
5cc6ab966f30atagen2026-10-11 11:23 UTC1265
            &jelq_app,
5cc6ab966f30atagen2026-10-11 11:23 UTC1266
            "POST",
5cc6ab966f30atagen2026-10-11 11:23 UTC1267
            "/runs/cafebabe/kitchen/watermelon",
5cc6ab966f30atagen2026-10-11 11:23 UTC1268
            &[],
5cc6ab966f30atagen2026-10-11 11:23 UTC1269
            "",
5cc6ab966f30atagen2026-10-11 11:23 UTC1270
        )
5cc6ab966f30atagen2026-10-11 11:23 UTC1271
        .await;
5cc6ab966f30atagen2026-10-11 11:23 UTC1272
        assert_eq!(
5cc6ab966f30atagen2026-10-11 11:23 UTC1273
            bite.status(),
5cc6ab966f30atagen2026-10-11 11:23 UTC1274
            StatusCode::CONFLICT,
5cc6ab966f30atagen2026-10-11 11:23 UTC1275
            "seed {seed} kept the melon through a jelq"
5cc6ab966f30atagen2026-10-11 11:23 UTC1276
        );
5cc6ab966f30atagen2026-10-11 11:23 UTC1277
5cc6ab966f30atagen2026-10-11 11:23 UTC1278
        let hold_app = app_with(Floor::Bedroom, seed, |run| {
5cc6ab966f30atagen2026-10-11 11:23 UTC1279
            run.stamina = 2;
5cc6ab966f30atagen2026-10-11 11:23 UTC1280
            run.here.melon_offered = true;
5cc6ab966f30atagen2026-10-11 11:23 UTC1281
        });
5cc6ab966f30atagen2026-10-11 11:23 UTC1282
        let held = send(&hold_app, "HOLD", "/runs/cafebabe", &[], "").await;
5cc6ab966f30atagen2026-10-11 11:23 UTC1283
        if held.status() != StatusCode::NO_CONTENT {
5cc6ab966f30atagen2026-10-11 11:23 UTC1284
            continue;
5cc6ab966f30atagen2026-10-11 11:23 UTC1285
        }
5cc6ab966f30atagen2026-10-11 11:23 UTC1286
        let bite = send(
5cc6ab966f30atagen2026-10-11 11:23 UTC1287
            &hold_app,
5cc6ab966f30atagen2026-10-11 11:23 UTC1288
            "POST",
5cc6ab966f30atagen2026-10-11 11:23 UTC1289
            "/runs/cafebabe/kitchen/watermelon",
5cc6ab966f30atagen2026-10-11 11:23 UTC1290
            &[],
5cc6ab966f30atagen2026-10-11 11:23 UTC1291
            "",
5cc6ab966f30atagen2026-10-11 11:23 UTC1292
        )
5cc6ab966f30atagen2026-10-11 11:23 UTC1293
        .await;
5cc6ab966f30atagen2026-10-11 11:23 UTC1294
        assert_eq!(
5cc6ab966f30atagen2026-10-11 11:23 UTC1295
            bite.status(),
5cc6ab966f30atagen2026-10-11 11:23 UTC1296
            StatusCode::CONFLICT,
5cc6ab966f30atagen2026-10-11 11:23 UTC1297
            "seed {seed} kept the melon through a hold"
5cc6ab966f30atagen2026-10-11 11:23 UTC1298
        );
5cc6ab966f30atagen2026-10-11 11:23 UTC1299
5cc6ab966f30atagen2026-10-11 11:23 UTC1300
        let read_app = app_with(Floor::Bedroom, seed, |run| {
5cc6ab966f30atagen2026-10-11 11:23 UTC1301
            run.story = Story::ClosedTab;
5cc6ab966f30atagen2026-10-11 11:23 UTC1302
            run.here.melon_offered = true;
5cc6ab966f30atagen2026-10-11 11:23 UTC1303
        });
5cc6ab966f30atagen2026-10-11 11:23 UTC1304
        let read = send(&read_app, "POST", "/runs/cafebabe/re-read", &[], "").await;
5cc6ab966f30atagen2026-10-11 11:23 UTC1305
        if read.status() == StatusCode::INTERNAL_SERVER_ERROR {
5cc6ab966f30atagen2026-10-11 11:23 UTC1306
            continue;
5cc6ab966f30atagen2026-10-11 11:23 UTC1307
        }
5cc6ab966f30atagen2026-10-11 11:23 UTC1308
        let bite = send(
5cc6ab966f30atagen2026-10-11 11:23 UTC1309
            &read_app,
5cc6ab966f30atagen2026-10-11 11:23 UTC1310
            "POST",
5cc6ab966f30atagen2026-10-11 11:23 UTC1311
            "/runs/cafebabe/kitchen/watermelon",
5cc6ab966f30atagen2026-10-11 11:23 UTC1312
            &[],
5cc6ab966f30atagen2026-10-11 11:23 UTC1313
            "",
5cc6ab966f30atagen2026-10-11 11:23 UTC1314
        )
5cc6ab966f30atagen2026-10-11 11:23 UTC1315
        .await;
5cc6ab966f30atagen2026-10-11 11:23 UTC1316
        assert_eq!(
5cc6ab966f30atagen2026-10-11 11:23 UTC1317
            bite.status(),
5cc6ab966f30atagen2026-10-11 11:23 UTC1318
            StatusCode::CONFLICT,
5cc6ab966f30atagen2026-10-11 11:23 UTC1319
            "seed {seed} kept the melon through a re-read"
5cc6ab966f30atagen2026-10-11 11:23 UTC1320
        );
5cc6ab966f30atagen2026-10-11 11:23 UTC1321
    }
5cc6ab966f30atagen2026-10-11 11:23 UTC1322
5cc6ab966f30atagen2026-10-11 11:23 UTC1323
    let mut stood = 0;
5cc6ab966f30atagen2026-10-11 11:23 UTC1324
    for seed in 0..60u64 {
5cc6ab966f30atagen2026-10-11 11:23 UTC1325
        let mut run = Run::new("cafebabe".into(), "cafe".into(), seed);
5cc6ab966f30atagen2026-10-11 11:23 UTC1326
        run.eq = 95;
5cc6ab966f30atagen2026-10-11 11:23 UTC1327
        run.here.melon_offered = true;
5cc6ab966f30atagen2026-10-11 11:23 UTC1328
        if run.raid_kitchen().is_err() {
5cc6ab966f30atagen2026-10-11 11:23 UTC1329
            continue;
5cc6ab966f30atagen2026-10-11 11:23 UTC1330
        }
5cc6ab966f30atagen2026-10-11 11:23 UTC1331
        stood += 1;
5cc6ab966f30atagen2026-10-11 11:23 UTC1332
        assert!(
5cc6ab966f30atagen2026-10-11 11:23 UTC1333
            run.here.melon_offered,
5cc6ab966f30atagen2026-10-11 11:23 UTC1334
            "seed {seed} lost the melon to another raid"
5cc6ab966f30atagen2026-10-11 11:23 UTC1335
        );
5cc6ab966f30atagen2026-10-11 11:23 UTC1336
    }
5cc6ab966f30atagen2026-10-11 11:23 UTC1337
    assert!(stood > 40, "raids at eq 95 rarely rupture");
5cc6ab966f30atagen2026-10-11 11:23 UTC1338
}
5cc6ab966f30atagen2026-10-11 11:23 UTC1339
5cc6ab966f30atagen2026-10-11 11:23 UTC1340
#[tokio::test]
0a0bfee8329eatagen2026-10-11 05:11 UTC1341
async fn the_kitchen_closes_after_two_tellings() {
0a0bfee8329eatagen2026-10-11 05:11 UTC1342
    let mut closed = None;
0a0bfee8329eatagen2026-10-11 05:11 UTC1343
    for seed in 0..300u64 {
0a0bfee8329eatagen2026-10-11 05:11 UTC1344
        let mut run = Run::new("cafebabe".into(), "cafe".into(), seed);
0a0bfee8329eatagen2026-10-11 05:11 UTC1345
        let first = run.raid_kitchen();
0a0bfee8329eatagen2026-10-11 05:11 UTC1346
        let second = run.raid_kitchen();
0a0bfee8329eatagen2026-10-11 05:11 UTC1347
        if second.is_err() && first.is_ok() {
0a0bfee8329eatagen2026-10-11 05:11 UTC1348
            assert_eq!(run.kitchen_tellings, 2);
0a0bfee8329eatagen2026-10-11 05:11 UTC1349
            let Err(third) = run.raid_kitchen() else {
0a0bfee8329eatagen2026-10-11 05:11 UTC1350
                panic!("the kitchen should be gone");
0a0bfee8329eatagen2026-10-11 05:11 UTC1351
            };
0a0bfee8329eatagen2026-10-11 05:11 UTC1352
            assert_eq!(third.status(), StatusCode::GONE);
0a0bfee8329eatagen2026-10-11 05:11 UTC1353
            closed = Some(seed);
0a0bfee8329eatagen2026-10-11 05:11 UTC1354
            break;
0a0bfee8329eatagen2026-10-11 05:11 UTC1355
        }
0a0bfee8329eatagen2026-10-11 05:11 UTC1356
    }
0a0bfee8329eatagen2026-10-11 05:11 UTC1357
    assert!(
0a0bfee8329eatagen2026-10-11 05:11 UTC1358
        closed.is_some(),
0a0bfee8329eatagen2026-10-11 05:11 UTC1359
        "two tellings should occur within 300 seeds"
0a0bfee8329eatagen2026-10-11 05:11 UTC1360
    );
0a0bfee8329eatagen2026-10-11 05:11 UTC1361
}
0a0bfee8329eatagen2026-10-11 05:11 UTC1362
0a0bfee8329eatagen2026-10-11 05:11 UTC1363
#[tokio::test]
0a0bfee8329eatagen2026-10-11 05:11 UTC1364
async fn lubes_run_out_but_spit_never_does() {
0a0bfee8329eatagen2026-10-11 05:11 UTC1365
    let app = seeded_with(Floor::Bedroom, |run| {
0a0bfee8329eatagen2026-10-11 05:11 UTC1366
        run.warmed_up = true;
0a0bfee8329eatagen2026-10-11 05:11 UTC1367
        run.items.baby_oil = 1;
0a0bfee8329eatagen2026-10-11 05:11 UTC1368
    });
0a0bfee8329eatagen2026-10-11 05:11 UTC1369
    let lube = [("content-type", "application/baby-oil")];
0a0bfee8329eatagen2026-10-11 05:11 UTC1370
    let first = send(&app, "JELQ", "/runs/cafebabe?technique=wet", &lube, "").await;
0a0bfee8329eatagen2026-10-11 05:11 UTC1371
    assert_ne!(first.status(), StatusCode::PRECONDITION_FAILED);
0a0bfee8329eatagen2026-10-11 05:11 UTC1372
    let second = send(&app, "JELQ", "/runs/cafebabe?technique=wet", &lube, "").await;
139164489032atagen2026-10-11 05:46 UTC1373
    assert_eq!(second.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
0a0bfee8329eatagen2026-10-11 05:11 UTC1374
0a0bfee8329eatagen2026-10-11 05:11 UTC1375
    let spit = [("content-type", "text/plain; charset=spit")];
0a0bfee8329eatagen2026-10-11 05:11 UTC1376
    for _ in 0..3 {
0a0bfee8329eatagen2026-10-11 05:11 UTC1377
        let wet = send(&app, "JELQ", "/runs/cafebabe?technique=wet", &spit, "").await;
0a0bfee8329eatagen2026-10-11 05:11 UTC1378
        assert_ne!(wet.status(), StatusCode::PRECONDITION_FAILED);
0a0bfee8329eatagen2026-10-11 05:11 UTC1379
    }
0a0bfee8329eatagen2026-10-11 05:11 UTC1380
}
0a0bfee8329eatagen2026-10-11 05:11 UTC1381
0a0bfee8329eatagen2026-10-11 05:11 UTC1382
#[tokio::test]
0a0bfee8329eatagen2026-10-11 05:11 UTC1383
async fn energy_drinks_add_three_stamina() {
0a0bfee8329eatagen2026-10-11 05:11 UTC1384
    let app = seeded_with(Floor::Bedroom, |run| {
0a0bfee8329eatagen2026-10-11 05:11 UTC1385
        run.stamina = 2;
0a0bfee8329eatagen2026-10-11 05:11 UTC1386
        run.items.energy_drinks = 1;
0a0bfee8329eatagen2026-10-11 05:11 UTC1387
    });
0a0bfee8329eatagen2026-10-11 05:11 UTC1388
    let view = send(&app, "GET", "/runs/cafebabe", &[], "").await;
0a0bfee8329eatagen2026-10-11 05:11 UTC1389
    assert!(header(&view, "link").contains("technique=energy"));
0a0bfee8329eatagen2026-10-11 05:11 UTC1390
    let drink = send(&app, "JELQ", "/runs/cafebabe?technique=energy", &[], "").await;
0a0bfee8329eatagen2026-10-11 05:11 UTC1391
    assert_eq!(drink.status(), StatusCode::OK);
0a0bfee8329eatagen2026-10-11 05:11 UTC1392
    let view = send(&app, "GET", "/runs/cafebabe", &[], "").await;
0a0bfee8329eatagen2026-10-11 05:11 UTC1393
    assert_eq!(header(&view, "x-stamina"), "5/10");
0a0bfee8329eatagen2026-10-11 05:11 UTC1394
    let again = send(&app, "JELQ", "/runs/cafebabe?technique=energy", &[], "").await;
0a0bfee8329eatagen2026-10-11 05:11 UTC1395
    assert_eq!(again.status(), StatusCode::FAILED_DEPENDENCY);
0a0bfee8329eatagen2026-10-11 05:11 UTC1396
}
0a0bfee8329eatagen2026-10-11 05:11 UTC1397
0a0bfee8329eatagen2026-10-11 05:11 UTC1398
#[tokio::test]
0a0bfee8329eatagen2026-10-11 05:11 UTC1399
async fn the_working_supplement_resets_stamina() {
0a0bfee8329eatagen2026-10-11 05:11 UTC1400
    let mut worked = 0;
0a0bfee8329eatagen2026-10-11 05:11 UTC1401
    let mut bunk = 0;
0a0bfee8329eatagen2026-10-11 05:11 UTC1402
    for seed in 0..60u64 {
0a0bfee8329eatagen2026-10-11 05:11 UTC1403
        let app = app_with(Floor::Bedroom, seed, |run| {
0a0bfee8329eatagen2026-10-11 05:11 UTC1404
            run.stamina = 1;
0a0bfee8329eatagen2026-10-11 05:11 UTC1405
            run.items.supplements = 1;
0a0bfee8329eatagen2026-10-11 05:11 UTC1406
        });
0a0bfee8329eatagen2026-10-11 05:11 UTC1407
        let taken = send(&app, "JELQ", "/runs/cafebabe?technique=supplement", &[], "").await;
0a0bfee8329eatagen2026-10-11 05:11 UTC1408
        if taken.status() == StatusCode::OK {
0a0bfee8329eatagen2026-10-11 05:11 UTC1409
            worked += 1;
0a0bfee8329eatagen2026-10-11 05:11 UTC1410
            let view = send(&app, "GET", "/runs/cafebabe", &[], "").await;
0a0bfee8329eatagen2026-10-11 05:11 UTC1411
            assert_eq!(header(&view, "x-stamina"), "10/10");
0a0bfee8329eatagen2026-10-11 05:11 UTC1412
        } else {
0a0bfee8329eatagen2026-10-11 05:11 UTC1413
            assert_eq!(taken.status(), StatusCode::BAD_GATEWAY);
0a0bfee8329eatagen2026-10-11 05:11 UTC1414
            bunk += 1;
0a0bfee8329eatagen2026-10-11 05:11 UTC1415
        }
0a0bfee8329eatagen2026-10-11 05:11 UTC1416
    }
0a0bfee8329eatagen2026-10-11 05:11 UTC1417
    assert!(worked > 0 && bunk > 0, "one in five should work too well");
0a0bfee8329eatagen2026-10-11 05:11 UTC1418
}
5dfebcc0ac43atagen2026-10-11 05:28 UTC1419
5dfebcc0ac43atagen2026-10-11 05:28 UTC1420
#[tokio::test]
5dfebcc0ac43atagen2026-10-11 05:28 UTC1421
async fn dry_jelqs_build_friction_and_real_lube_does_not() {
5dfebcc0ac43atagen2026-10-11 05:28 UTC1422
    let app = seeded_with(Floor::Bedroom, |run| {
5dfebcc0ac43atagen2026-10-11 05:28 UTC1423
        run.warmed_up = true;
058650ec79d6atagen2026-10-12 10:49 UTC1424
        run.foreskin = Some(0.9);
5dfebcc0ac43atagen2026-10-11 05:28 UTC1425
    });
5dfebcc0ac43atagen2026-10-11 05:28 UTC1426
    let dry = send(&app, "JELQ", "/runs/cafebabe?technique=dry", &[], "").await;
5dfebcc0ac43atagen2026-10-11 05:28 UTC1427
    assert_eq!(header(&dry, "x-friction"), "2/10");
5dfebcc0ac43atagen2026-10-11 05:28 UTC1428
    let lube = [("content-type", "application/baby-oil")];
5dfebcc0ac43atagen2026-10-11 05:28 UTC1429
    let wet = send(
5dfebcc0ac43atagen2026-10-11 05:28 UTC1430
        &app,
5dfebcc0ac43atagen2026-10-11 05:28 UTC1431
        "JELQ",
5dfebcc0ac43atagen2026-10-11 05:28 UTC1432
        "/runs/cafebabe?technique=wet&intensity=5",
5dfebcc0ac43atagen2026-10-11 05:28 UTC1433
        &lube,
5dfebcc0ac43atagen2026-10-11 05:28 UTC1434
        "",
5dfebcc0ac43atagen2026-10-11 05:28 UTC1435
    )
5dfebcc0ac43atagen2026-10-11 05:28 UTC1436
    .await;
5dfebcc0ac43atagen2026-10-11 05:28 UTC1437
    assert_eq!(header(&wet, "x-friction"), "2/10");
5dfebcc0ac43atagen2026-10-11 05:28 UTC1438
    let spit = [("content-type", "text/plain; charset=spit")];
5dfebcc0ac43atagen2026-10-11 05:28 UTC1439
    let glisten = send(
5dfebcc0ac43atagen2026-10-11 05:28 UTC1440
        &app,
5dfebcc0ac43atagen2026-10-11 05:28 UTC1441
        "JELQ",
5dfebcc0ac43atagen2026-10-11 05:28 UTC1442
        "/runs/cafebabe?technique=wet&intensity=5",
5dfebcc0ac43atagen2026-10-11 05:28 UTC1443
        &spit,
5dfebcc0ac43atagen2026-10-11 05:28 UTC1444
        "",
5dfebcc0ac43atagen2026-10-11 05:28 UTC1445
    )
5dfebcc0ac43atagen2026-10-11 05:28 UTC1446
    .await;
5dfebcc0ac43atagen2026-10-11 05:28 UTC1447
    assert_eq!(header(&glisten, "x-friction"), "3/10");
5dfebcc0ac43atagen2026-10-11 05:28 UTC1448
}
5dfebcc0ac43atagen2026-10-11 05:28 UTC1449
5dfebcc0ac43atagen2026-10-11 05:28 UTC1450
#[tokio::test]
5dfebcc0ac43atagen2026-10-11 05:28 UTC1451
async fn switching_grips_makes_the_friction_cheaper() {
5dfebcc0ac43atagen2026-10-11 05:28 UTC1452
    let app = seeded_with(Floor::Bedroom, |run| {
5dfebcc0ac43atagen2026-10-11 05:28 UTC1453
        run.warmed_up = true;
058650ec79d6atagen2026-10-12 10:49 UTC1454
        run.foreskin = Some(0.9);
5dfebcc0ac43atagen2026-10-11 05:28 UTC1455
    });
5dfebcc0ac43atagen2026-10-11 05:28 UTC1456
    let dry = send(&app, "JELQ", "/runs/cafebabe?technique=dry", &[], "").await;
5dfebcc0ac43atagen2026-10-11 05:28 UTC1457
    assert_eq!(header(&dry, "x-friction"), "2/10");
5dfebcc0ac43atagen2026-10-11 05:28 UTC1458
    send(
5dfebcc0ac43atagen2026-10-11 05:28 UTC1459
        &app,
5dfebcc0ac43atagen2026-10-11 05:28 UTC1460
        "GET",
5dfebcc0ac43atagen2026-10-11 05:28 UTC1461
        "/runs/cafebabe/grip",
5dfebcc0ac43atagen2026-10-11 05:28 UTC1462
        &[("upgrade", "underhand"), ("connection", "upgrade")],
5dfebcc0ac43atagen2026-10-11 05:28 UTC1463
        "",
5dfebcc0ac43atagen2026-10-11 05:28 UTC1464
    )
5dfebcc0ac43atagen2026-10-11 05:28 UTC1465
    .await;
5dfebcc0ac43atagen2026-10-11 05:28 UTC1466
    let switched = send(&app, "JELQ", "/runs/cafebabe?technique=dry", &[], "").await;
5dfebcc0ac43atagen2026-10-11 05:28 UTC1467
    assert_eq!(header(&switched, "x-friction"), "3/10");
5dfebcc0ac43atagen2026-10-11 05:28 UTC1468
    let settled = send(&app, "JELQ", "/runs/cafebabe?technique=dry", &[], "").await;
5dfebcc0ac43atagen2026-10-11 05:28 UTC1469
    assert_eq!(header(&settled, "x-friction"), "5/10");
5dfebcc0ac43atagen2026-10-11 05:28 UTC1470
}
5dfebcc0ac43atagen2026-10-11 05:28 UTC1471
5dfebcc0ac43atagen2026-10-11 05:28 UTC1472
#[tokio::test]
5dfebcc0ac43atagen2026-10-11 05:28 UTC1473
async fn holding_and_meditating_shed_friction() {
5dfebcc0ac43atagen2026-10-11 05:28 UTC1474
    let app = seeded_with(Floor::Bedroom, |run| {
5dfebcc0ac43atagen2026-10-11 05:28 UTC1475
        run.eq = 80;
5dfebcc0ac43atagen2026-10-11 05:28 UTC1476
        run.friction = 5;
5dfebcc0ac43atagen2026-10-11 05:28 UTC1477
    });
5dfebcc0ac43atagen2026-10-11 05:28 UTC1478
    let held = send(&app, "HOLD", "/runs/cafebabe", &[], "").await;
5dfebcc0ac43atagen2026-10-11 05:28 UTC1479
    assert_eq!(held.status(), StatusCode::NO_CONTENT);
5dfebcc0ac43atagen2026-10-11 05:28 UTC1480
    let view = send(&app, "GET", "/runs/cafebabe", &[], "").await;
5dfebcc0ac43atagen2026-10-11 05:28 UTC1481
    assert_eq!(header(&view, "x-friction"), "3/10");
5dfebcc0ac43atagen2026-10-11 05:28 UTC1482
5dfebcc0ac43atagen2026-10-11 05:28 UTC1483
    let taught = seeded_with(Floor::Bedroom, |run| {
5dfebcc0ac43atagen2026-10-11 05:28 UTC1484
        run.friction = 5;
5dfebcc0ac43atagen2026-10-11 05:28 UTC1485
    });
5dfebcc0ac43atagen2026-10-11 05:28 UTC1486
    let calmed = send(
5dfebcc0ac43atagen2026-10-11 05:28 UTC1487
        &taught,
5dfebcc0ac43atagen2026-10-11 05:28 UTC1488
        "JELQ",
5dfebcc0ac43atagen2026-10-11 05:28 UTC1489
        "/runs/cafebabe?technique=meditate",
5dfebcc0ac43atagen2026-10-11 05:28 UTC1490
        &[],
5dfebcc0ac43atagen2026-10-11 05:28 UTC1491
        "",
5dfebcc0ac43atagen2026-10-11 05:28 UTC1492
    )
5dfebcc0ac43atagen2026-10-11 05:28 UTC1493
    .await;
5dfebcc0ac43atagen2026-10-11 05:28 UTC1494
    assert_eq!(calmed.status(), StatusCode::OK);
5dfebcc0ac43atagen2026-10-11 05:28 UTC1495
    let view = send(&taught, "GET", "/runs/cafebabe", &[], "").await;
5dfebcc0ac43atagen2026-10-11 05:28 UTC1496
    assert_eq!(header(&view, "x-friction"), "3/10");
5dfebcc0ac43atagen2026-10-11 05:28 UTC1497
}
5dfebcc0ac43atagen2026-10-11 05:28 UTC1498
5dfebcc0ac43atagen2026-10-11 05:28 UTC1499
#[tokio::test]
5dfebcc0ac43atagen2026-10-11 05:28 UTC1500
async fn a_friction_burn_halves_the_gains() {
5dfebcc0ac43atagen2026-10-11 05:28 UTC1501
    let lube = [("content-type", "application/baby-oil")];
5dfebcc0ac43atagen2026-10-11 05:28 UTC1502
    let path = "/runs/cafebabe?technique=wet&intensity=5";
5dfebcc0ac43atagen2026-10-11 05:28 UTC1503
    for seed in 0..60u64 {
5dfebcc0ac43atagen2026-10-11 05:28 UTC1504
        let steady = app_with(Floor::Bedroom, seed, |run| {
5dfebcc0ac43atagen2026-10-11 05:28 UTC1505
            run.warmed_up = true;
5dfebcc0ac43atagen2026-10-11 05:28 UTC1506
            run.eq = 60;
5dfebcc0ac43atagen2026-10-11 05:28 UTC1507
            run.items.baby_oil = 9;
5dfebcc0ac43atagen2026-10-11 05:28 UTC1508
        });
5dfebcc0ac43atagen2026-10-11 05:28 UTC1509
        let hurt_app = app_with(Floor::Bedroom, seed, |run| {
5dfebcc0ac43atagen2026-10-11 05:28 UTC1510
            run.warmed_up = true;
5dfebcc0ac43atagen2026-10-11 05:28 UTC1511
            run.eq = 60;
5dfebcc0ac43atagen2026-10-11 05:28 UTC1512
            run.items.baby_oil = 9;
5dfebcc0ac43atagen2026-10-11 05:28 UTC1513
            run.conditions.friction_burn = true;
5dfebcc0ac43atagen2026-10-11 05:28 UTC1514
        });
5dfebcc0ac43atagen2026-10-11 05:28 UTC1515
        let calm = send(&steady, "JELQ", path, &lube, "").await;
5dfebcc0ac43atagen2026-10-11 05:28 UTC1516
        let hurt = send(&hurt_app, "JELQ", path, &lube, "").await;
5dfebcc0ac43atagen2026-10-11 05:28 UTC1517
        if calm.status() != StatusCode::OK || hurt.status() != StatusCode::OK {
5dfebcc0ac43atagen2026-10-11 05:28 UTC1518
            continue;
5dfebcc0ac43atagen2026-10-11 05:28 UTC1519
        }
5dfebcc0ac43atagen2026-10-11 05:28 UTC1520
        let calm_gain = json(calm).await["length"].as_f64().unwrap();
5dfebcc0ac43atagen2026-10-11 05:28 UTC1521
        let hurt_gain = json(hurt).await["length"].as_f64().unwrap();
5dfebcc0ac43atagen2026-10-11 05:28 UTC1522
        assert!(
5dfebcc0ac43atagen2026-10-11 05:28 UTC1523
            (hurt_gain * 2.0 - calm_gain).abs() < 1e-9,
5dfebcc0ac43atagen2026-10-11 05:28 UTC1524
            "seed {seed} burned {hurt_gain} against {calm_gain}"
5dfebcc0ac43atagen2026-10-11 05:28 UTC1525
        );
5dfebcc0ac43atagen2026-10-11 05:28 UTC1526
        return;
5dfebcc0ac43atagen2026-10-11 05:28 UTC1527
    }
5dfebcc0ac43atagen2026-10-11 05:28 UTC1528
    panic!("no seed in 60 produced two clean rolls to compare");
5dfebcc0ac43atagen2026-10-11 05:28 UTC1529
}
5dfebcc0ac43atagen2026-10-11 05:28 UTC1530
5dfebcc0ac43atagen2026-10-11 05:28 UTC1531
#[tokio::test]
5dfebcc0ac43atagen2026-10-11 05:28 UTC1532
async fn the_burn_warns_and_the_ice_treats_it() {
5dfebcc0ac43atagen2026-10-11 05:28 UTC1533
    let app = seeded_with(Floor::Bedroom, |run| {
5dfebcc0ac43atagen2026-10-11 05:28 UTC1534
        run.warmed_up = true;
5dfebcc0ac43atagen2026-10-11 05:28 UTC1535
        run.eq = 60;
5dfebcc0ac43atagen2026-10-11 05:28 UTC1536
        run.items.baby_oil = 9;
5dfebcc0ac43atagen2026-10-11 05:28 UTC1537
        run.friction = 7;
5dfebcc0ac43atagen2026-10-11 05:28 UTC1538
        run.conditions.friction_burn = true;
5dfebcc0ac43atagen2026-10-11 05:28 UTC1539
    });
5dfebcc0ac43atagen2026-10-11 05:28 UTC1540
    let lube = [("content-type", "application/baby-oil")];
5dfebcc0ac43atagen2026-10-11 05:28 UTC1541
    let jelq = send(
5dfebcc0ac43atagen2026-10-11 05:28 UTC1542
        &app,
5dfebcc0ac43atagen2026-10-11 05:28 UTC1543
        "JELQ",
5dfebcc0ac43atagen2026-10-11 05:28 UTC1544
        "/runs/cafebabe?technique=wet&intensity=5",
5dfebcc0ac43atagen2026-10-11 05:28 UTC1545
        &lube,
5dfebcc0ac43atagen2026-10-11 05:28 UTC1546
        "",
5dfebcc0ac43atagen2026-10-11 05:28 UTC1547
    )
5dfebcc0ac43atagen2026-10-11 05:28 UTC1548
    .await;
5dfebcc0ac43atagen2026-10-11 05:28 UTC1549
    let warnings: Vec<_> = jelq
5dfebcc0ac43atagen2026-10-11 05:28 UTC1550
        .headers()
5dfebcc0ac43atagen2026-10-11 05:28 UTC1551
        .get_all("warning")
5dfebcc0ac43atagen2026-10-11 05:28 UTC1552
        .iter()
5dfebcc0ac43atagen2026-10-11 05:28 UTC1553
        .filter_map(|w| w.to_str().ok())
5dfebcc0ac43atagen2026-10-11 05:28 UTC1554
        .collect();
5dfebcc0ac43atagen2026-10-11 05:28 UTC1555
    assert!(warnings.contains(&"199 - \"friction burn\""));
5dfebcc0ac43atagen2026-10-11 05:28 UTC1556
    send(&app, "JELQ", "/runs/cafebabe?technique=ice", &[], "").await;
5dfebcc0ac43atagen2026-10-11 05:28 UTC1557
    let view = send(&app, "GET", "/runs/cafebabe", &[], "").await;
5dfebcc0ac43atagen2026-10-11 05:28 UTC1558
    let warnings: Vec<_> = view
5dfebcc0ac43atagen2026-10-11 05:28 UTC1559
        .headers()
5dfebcc0ac43atagen2026-10-11 05:28 UTC1560
        .get_all("warning")
5dfebcc0ac43atagen2026-10-11 05:28 UTC1561
        .iter()
5dfebcc0ac43atagen2026-10-11 05:28 UTC1562
        .filter_map(|w| w.to_str().ok())
5dfebcc0ac43atagen2026-10-11 05:28 UTC1563
        .collect();
5dfebcc0ac43atagen2026-10-11 05:28 UTC1564
    assert!(!warnings.contains(&"199 - \"friction burn\""));
5dfebcc0ac43atagen2026-10-11 05:28 UTC1565
}
139164489032atagen2026-10-11 05:46 UTC1566
139164489032atagen2026-10-11 05:46 UTC1567
#[tokio::test]
139164489032atagen2026-10-11 05:46 UTC1568
async fn the_burn_shifts_the_rupture_tiers() {
139164489032atagen2026-10-11 05:46 UTC1569
    async fn ruptures(burned: bool) -> u32 {
139164489032atagen2026-10-11 05:46 UTC1570
        let mut gone = 0;
139164489032atagen2026-10-11 05:46 UTC1571
        for seed in 0..400u64 {
139164489032atagen2026-10-11 05:46 UTC1572
            let app = app_with(Floor::Bedroom, seed, |run| {
139164489032atagen2026-10-11 05:46 UTC1573
                run.warmed_up = true;
139164489032atagen2026-10-11 05:46 UTC1574
                run.eq = 60;
139164489032atagen2026-10-11 05:46 UTC1575
                run.items.baby_oil = 9;
139164489032atagen2026-10-11 05:46 UTC1576
                run.conditions.friction_burn = burned;
139164489032atagen2026-10-11 05:46 UTC1577
            });
139164489032atagen2026-10-11 05:46 UTC1578
            let lube = [("content-type", "application/baby-oil")];
139164489032atagen2026-10-11 05:46 UTC1579
            let jelq = send(
139164489032atagen2026-10-11 05:46 UTC1580
                &app,
139164489032atagen2026-10-11 05:46 UTC1581
                "JELQ",
139164489032atagen2026-10-11 05:46 UTC1582
                "/runs/cafebabe?technique=wet&intensity=5",
139164489032atagen2026-10-11 05:46 UTC1583
                &lube,
139164489032atagen2026-10-11 05:46 UTC1584
                "",
139164489032atagen2026-10-11 05:46 UTC1585
            )
139164489032atagen2026-10-11 05:46 UTC1586
            .await;
139164489032atagen2026-10-11 05:46 UTC1587
            if jelq.status() == StatusCode::INTERNAL_SERVER_ERROR {
139164489032atagen2026-10-11 05:46 UTC1588
                gone += 1;
139164489032atagen2026-10-11 05:46 UTC1589
            }
139164489032atagen2026-10-11 05:46 UTC1590
        }
139164489032atagen2026-10-11 05:46 UTC1591
        gone
139164489032atagen2026-10-11 05:46 UTC1592
    }
139164489032atagen2026-10-11 05:46 UTC1593
    let burned = ruptures(true).await;
139164489032atagen2026-10-11 05:46 UTC1594
    let calm = ruptures(false).await;
139164489032atagen2026-10-11 05:46 UTC1595
    assert!(
139164489032atagen2026-10-11 05:46 UTC1596
        burned >= 8 && calm <= 4,
139164489032atagen2026-10-11 05:46 UTC1597
        "burned {burned} ruptures against {calm}"
139164489032atagen2026-10-11 05:46 UTC1598
    );
139164489032atagen2026-10-11 05:46 UTC1599
}
139164489032atagen2026-10-11 05:46 UTC1600
139164489032atagen2026-10-11 05:46 UTC1601
#[tokio::test]
139164489032atagen2026-10-11 05:46 UTC1602
async fn meditation_respects_the_priapism_floor() {
139164489032atagen2026-10-11 05:46 UTC1603
    let app = seeded_with(Floor::Bedroom, |run| {
139164489032atagen2026-10-11 05:46 UTC1604
        run.eq = 80;
139164489032atagen2026-10-11 05:46 UTC1605
        run.conditions.priapism_turns = 3;
139164489032atagen2026-10-11 05:46 UTC1606
    });
139164489032atagen2026-10-11 05:46 UTC1607
    let calmed = send(&app, "JELQ", "/runs/cafebabe?technique=meditate", &[], "").await;
139164489032atagen2026-10-11 05:46 UTC1608
    assert_eq!(calmed.status(), StatusCode::OK);
139164489032atagen2026-10-11 05:46 UTC1609
    let view = send(&app, "GET", "/runs/cafebabe", &[], "").await;
139164489032atagen2026-10-11 05:46 UTC1610
    assert_eq!(header(&view, "x-eq"), "75");
139164489032atagen2026-10-11 05:46 UTC1611
}
139164489032atagen2026-10-11 05:46 UTC1612
139164489032atagen2026-10-11 05:46 UTC1613
#[tokio::test]
139164489032atagen2026-10-11 05:46 UTC1614
async fn the_kitchen_pays_out_at_the_documented_odds() {
139164489032atagen2026-10-11 05:46 UTC1615
    let mut oil = 0;
139164489032atagen2026-10-11 05:46 UTC1616
    let mut drinks = 0;
139164489032atagen2026-10-11 05:46 UTC1617
    let mut packs = 0;
139164489032atagen2026-10-11 05:46 UTC1618
    for seed in 0..600u64 {
139164489032atagen2026-10-11 05:46 UTC1619
        let mut run = Run::new("cafebabe".into(), "cafe".into(), seed);
139164489032atagen2026-10-11 05:46 UTC1620
        run.kitchen_tellings = 0;
139164489032atagen2026-10-11 05:46 UTC1621
        let held = (
139164489032atagen2026-10-11 05:46 UTC1622
            run.items.coconut_oil,
139164489032atagen2026-10-11 05:46 UTC1623
            run.items.energy_drinks,
139164489032atagen2026-10-11 05:46 UTC1624
            run.items.ice_packs,
139164489032atagen2026-10-11 05:46 UTC1625
        );
139164489032atagen2026-10-11 05:46 UTC1626
        let _ = run.raid_kitchen();
139164489032atagen2026-10-11 05:46 UTC1627
        oil += u32::from(run.items.coconut_oil > held.0);
139164489032atagen2026-10-11 05:46 UTC1628
        drinks += u32::from(run.items.energy_drinks > held.1);
139164489032atagen2026-10-11 05:46 UTC1629
        packs += u32::from(run.items.ice_packs > held.2);
139164489032atagen2026-10-11 05:46 UTC1630
    }
139164489032atagen2026-10-11 05:46 UTC1631
    assert!((170..=230).contains(&oil), "coconut {oil} in 600");
139164489032atagen2026-10-11 05:46 UTC1632
    assert!((90..=150).contains(&drinks), "energy {drinks} in 600");
139164489032atagen2026-10-11 05:46 UTC1633
    assert!((45..=105).contains(&packs), "ice {packs} in 600");
139164489032atagen2026-10-11 05:46 UTC1634
}
139164489032atagen2026-10-11 05:46 UTC1635
139164489032atagen2026-10-11 05:46 UTC1636
#[tokio::test]
139164489032atagen2026-10-11 05:46 UTC1637
async fn a_refused_jelq_leaves_the_lube_alone() {
139164489032atagen2026-10-11 05:46 UTC1638
    let app = seeded_with(Floor::Bedroom, |run| {
139164489032atagen2026-10-11 05:46 UTC1639
        run.warmed_up = true;
139164489032atagen2026-10-11 05:46 UTC1640
        run.items.extender = true;
9374b3933189atagen2026-10-12 11:15 UTC1641
        run.girth = 6.0;
9374b3933189atagen2026-10-12 11:15 UTC1642
        run.length = 6.0;
139164489032atagen2026-10-11 05:46 UTC1643
    });
139164489032atagen2026-10-11 05:46 UTC1644
    let lube = [("content-type", "application/baby-oil")];
139164489032atagen2026-10-11 05:46 UTC1645
    let full = send(
139164489032atagen2026-10-11 05:46 UTC1646
        &app,
139164489032atagen2026-10-11 05:46 UTC1647
        "JELQ",
139164489032atagen2026-10-11 05:46 UTC1648
        "/runs/cafebabe?technique=wet&intensity=5",
139164489032atagen2026-10-11 05:46 UTC1649
        &lube,
139164489032atagen2026-10-11 05:46 UTC1650
        "",
139164489032atagen2026-10-11 05:46 UTC1651
    )
139164489032atagen2026-10-11 05:46 UTC1652
    .await;
139164489032atagen2026-10-11 05:46 UTC1653
    assert_eq!(full.status(), StatusCode::INSUFFICIENT_STORAGE);
139164489032atagen2026-10-11 05:46 UTC1654
    let inv = send(&app, "GET", "/runs/cafebabe/inventory", &[], "").await;
139164489032atagen2026-10-11 05:46 UTC1655
    let body = json(inv).await;
139164489032atagen2026-10-11 05:46 UTC1656
    let held = body["items"].as_array().unwrap();
139164489032atagen2026-10-11 05:46 UTC1657
    assert!(held.iter().any(|i| i["name"] == "baby-oil"));
139164489032atagen2026-10-11 05:46 UTC1658
}
14de3e1e8dc2atagen2026-10-11 06:19 UTC1659
14de3e1e8dc2atagen2026-10-11 06:19 UTC1660
#[tokio::test]
14de3e1e8dc2atagen2026-10-11 06:19 UTC1661
async fn an_empty_bottle_is_declined_before_the_enemy_stirs() {
14de3e1e8dc2atagen2026-10-11 06:19 UTC1662
    let control = seeded_with(Floor::ChangingRoom, |run| {
14de3e1e8dc2atagen2026-10-11 06:19 UTC1663
        run.warmed_up = true;
14de3e1e8dc2atagen2026-10-11 06:19 UTC1664
        run.suspicion = SUSPICION_LIMIT;
14de3e1e8dc2atagen2026-10-11 06:19 UTC1665
    });
14de3e1e8dc2atagen2026-10-11 06:19 UTC1666
    let lube = [("content-type", "application/baby-oil")];
14de3e1e8dc2atagen2026-10-11 06:19 UTC1667
    let caught = send(
14de3e1e8dc2atagen2026-10-11 06:19 UTC1668
        &control,
14de3e1e8dc2atagen2026-10-11 06:19 UTC1669
        "JELQ",
14de3e1e8dc2atagen2026-10-11 06:19 UTC1670
        "/runs/cafebabe?technique=wet&intensity=5",
14de3e1e8dc2atagen2026-10-11 06:19 UTC1671
        &lube,
14de3e1e8dc2atagen2026-10-11 06:19 UTC1672
        "",
14de3e1e8dc2atagen2026-10-11 06:19 UTC1673
    )
14de3e1e8dc2atagen2026-10-11 06:19 UTC1674
    .await;
14de3e1e8dc2atagen2026-10-11 06:19 UTC1675
    assert_eq!(caught.status(), StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS);
14de3e1e8dc2atagen2026-10-11 06:19 UTC1676
14de3e1e8dc2atagen2026-10-11 06:19 UTC1677
    let app = seeded_with(Floor::ChangingRoom, |run| {
14de3e1e8dc2atagen2026-10-11 06:19 UTC1678
        run.warmed_up = true;
14de3e1e8dc2atagen2026-10-11 06:19 UTC1679
        run.suspicion = SUSPICION_LIMIT;
14de3e1e8dc2atagen2026-10-11 06:19 UTC1680
        run.items.baby_oil = 0;
14de3e1e8dc2atagen2026-10-11 06:19 UTC1681
    });
14de3e1e8dc2atagen2026-10-11 06:19 UTC1682
    let refused = send(
14de3e1e8dc2atagen2026-10-11 06:19 UTC1683
        &app,
14de3e1e8dc2atagen2026-10-11 06:19 UTC1684
        "JELQ",
14de3e1e8dc2atagen2026-10-11 06:19 UTC1685
        "/runs/cafebabe?technique=wet&intensity=5",
14de3e1e8dc2atagen2026-10-11 06:19 UTC1686
        &lube,
14de3e1e8dc2atagen2026-10-11 06:19 UTC1687
        "",
14de3e1e8dc2atagen2026-10-11 06:19 UTC1688
    )
14de3e1e8dc2atagen2026-10-11 06:19 UTC1689
    .await;
14de3e1e8dc2atagen2026-10-11 06:19 UTC1690
    assert_eq!(refused.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
14de3e1e8dc2atagen2026-10-11 06:19 UTC1691
    let view = send(&app, "GET", "/runs/cafebabe", &[], "").await;
14de3e1e8dc2atagen2026-10-11 06:19 UTC1692
    assert_eq!(header(&view, "x-suspicion"), "5/5");
14de3e1e8dc2atagen2026-10-11 06:19 UTC1693
    assert_eq!(header(&view, "x-shame"), "0");
14de3e1e8dc2atagen2026-10-11 06:19 UTC1694
}
14de3e1e8dc2atagen2026-10-11 06:19 UTC1695
14de3e1e8dc2atagen2026-10-11 06:19 UTC1696
#[tokio::test]
14de3e1e8dc2atagen2026-10-11 06:19 UTC1697
async fn the_top_tier_rolls_a_single_third() {
14de3e1e8dc2atagen2026-10-11 06:19 UTC1698
    let mut ruptures = 0;
14de3e1e8dc2atagen2026-10-11 06:19 UTC1699
    let mut early = 0;
14de3e1e8dc2atagen2026-10-11 06:19 UTC1700
    for seed in 0..400u64 {
14de3e1e8dc2atagen2026-10-11 06:19 UTC1701
        let app = app_with(Floor::Bedroom, seed, |run| {
14de3e1e8dc2atagen2026-10-11 06:19 UTC1702
            run.warmed_up = true;
14de3e1e8dc2atagen2026-10-11 06:19 UTC1703
            run.eq = 96;
14de3e1e8dc2atagen2026-10-11 06:19 UTC1704
            run.items.baby_oil = 9;
14de3e1e8dc2atagen2026-10-11 06:19 UTC1705
            run.items.blinds = true;
14de3e1e8dc2atagen2026-10-11 06:19 UTC1706
        });
14de3e1e8dc2atagen2026-10-11 06:19 UTC1707
        let lube = [("content-type", "application/baby-oil")];
14de3e1e8dc2atagen2026-10-11 06:19 UTC1708
        let jelq = send(
14de3e1e8dc2atagen2026-10-11 06:19 UTC1709
            &app,
14de3e1e8dc2atagen2026-10-11 06:19 UTC1710
            "JELQ",
14de3e1e8dc2atagen2026-10-11 06:19 UTC1711
            "/runs/cafebabe?technique=wet&intensity=5",
14de3e1e8dc2atagen2026-10-11 06:19 UTC1712
            &lube,
14de3e1e8dc2atagen2026-10-11 06:19 UTC1713
            "",
14de3e1e8dc2atagen2026-10-11 06:19 UTC1714
        )
14de3e1e8dc2atagen2026-10-11 06:19 UTC1715
        .await;
14de3e1e8dc2atagen2026-10-11 06:19 UTC1716
        match jelq.status() {
14de3e1e8dc2atagen2026-10-11 06:19 UTC1717
            StatusCode::INTERNAL_SERVER_ERROR => ruptures += 1,
14de3e1e8dc2atagen2026-10-11 06:19 UTC1718
            StatusCode::TOO_EARLY => early += 1,
14de3e1e8dc2atagen2026-10-11 06:19 UTC1719
            other => panic!("seed {seed} ended in {other}"),
14de3e1e8dc2atagen2026-10-11 06:19 UTC1720
        }
14de3e1e8dc2atagen2026-10-11 06:19 UTC1721
    }
14de3e1e8dc2atagen2026-10-11 06:19 UTC1722
    assert!(
14de3e1e8dc2atagen2026-10-11 06:19 UTC1723
        (90..=175).contains(&ruptures),
14de3e1e8dc2atagen2026-10-11 06:19 UTC1724
        "ruptures {ruptures} of 400 against {early} early"
14de3e1e8dc2atagen2026-10-11 06:19 UTC1725
    );
14de3e1e8dc2atagen2026-10-11 06:19 UTC1726
    assert!(
14de3e1e8dc2atagen2026-10-11 06:19 UTC1727
        (225..=310).contains(&early),
14de3e1e8dc2atagen2026-10-11 06:19 UTC1728
        "early {early} of 400 against {ruptures} ruptures"
14de3e1e8dc2atagen2026-10-11 06:19 UTC1729
    );
14de3e1e8dc2atagen2026-10-11 06:19 UTC1730
}
14de3e1e8dc2atagen2026-10-11 06:19 UTC1731
14de3e1e8dc2atagen2026-10-11 06:19 UTC1732
#[tokio::test]
14de3e1e8dc2atagen2026-10-11 06:19 UTC1733
async fn priapism_holds_the_floor_through_every_reset() {
14de3e1e8dc2atagen2026-10-11 06:19 UTC1734
    let exit = seeded_with(Floor::Bedroom, |run| {
14de3e1e8dc2atagen2026-10-11 06:19 UTC1735
        run.warmed_up = true;
14de3e1e8dc2atagen2026-10-11 06:19 UTC1736
        run.eq = 90;
14de3e1e8dc2atagen2026-10-11 06:19 UTC1737
        run.here.turns = 99;
14de3e1e8dc2atagen2026-10-11 06:19 UTC1738
        run.conditions.priapism_turns = 3;
14de3e1e8dc2atagen2026-10-11 06:19 UTC1739
    });
14de3e1e8dc2atagen2026-10-11 06:19 UTC1740
    let out = send(&exit, "POST", "/runs/cafebabe/floor", &[], "").await;
14de3e1e8dc2atagen2026-10-11 06:19 UTC1741
    assert_eq!(out.status(), StatusCode::SEE_OTHER);
14de3e1e8dc2atagen2026-10-11 06:19 UTC1742
    let view = send(&exit, "GET", "/runs/cafebabe", &[], "").await;
14de3e1e8dc2atagen2026-10-11 06:19 UTC1743
    assert_eq!(header(&view, "x-eq"), "75");
14de3e1e8dc2atagen2026-10-11 06:19 UTC1744
14de3e1e8dc2atagen2026-10-11 06:19 UTC1745
    let melon = seeded_with(Floor::Bedroom, |run| {
14de3e1e8dc2atagen2026-10-11 06:19 UTC1746
        run.eq = 95;
14de3e1e8dc2atagen2026-10-11 06:19 UTC1747
        run.here.melon_offered = true;
14de3e1e8dc2atagen2026-10-11 06:19 UTC1748
        run.conditions.priapism_turns = 2;
14de3e1e8dc2atagen2026-10-11 06:19 UTC1749
    });
14de3e1e8dc2atagen2026-10-11 06:19 UTC1750
    let bite = send(&melon, "POST", "/runs/cafebabe/kitchen/watermelon", &[], "").await;
14de3e1e8dc2atagen2026-10-11 06:19 UTC1751
    assert_eq!(bite.status(), StatusCode::OK);
14de3e1e8dc2atagen2026-10-11 06:19 UTC1752
    let view = send(&melon, "GET", "/runs/cafebabe", &[], "").await;
14de3e1e8dc2atagen2026-10-11 06:19 UTC1753
    assert_eq!(header(&view, "x-eq"), "75");
14de3e1e8dc2atagen2026-10-11 06:19 UTC1754
}
b85a0d4536c3atagen2026-10-11 08:50 UTC1755
b85a0d4536c3atagen2026-10-11 08:50 UTC1756
#[tokio::test]
b85a0d4536c3atagen2026-10-11 08:50 UTC1757
async fn holding_rolls_a_flat_rupture_chance() {
b85a0d4536c3atagen2026-10-11 08:50 UTC1758
    async fn ruptures(eq: i32, stamina: i32) -> u32 {
b85a0d4536c3atagen2026-10-11 08:50 UTC1759
        let mut gone = 0;
b85a0d4536c3atagen2026-10-11 08:50 UTC1760
        for seed in 0..600u64 {
b85a0d4536c3atagen2026-10-11 08:50 UTC1761
            let app = app_with(Floor::Bedroom, seed, |run| {
b85a0d4536c3atagen2026-10-11 08:50 UTC1762
                run.warmed_up = true;
b85a0d4536c3atagen2026-10-11 08:50 UTC1763
                run.eq = eq;
b85a0d4536c3atagen2026-10-11 08:50 UTC1764
                run.stamina = stamina;
b85a0d4536c3atagen2026-10-11 08:50 UTC1765
            });
b85a0d4536c3atagen2026-10-11 08:50 UTC1766
            let held = send(&app, "HOLD", "/runs/cafebabe", &[], "").await;
b85a0d4536c3atagen2026-10-11 08:50 UTC1767
            if held.status() == StatusCode::INTERNAL_SERVER_ERROR {
b85a0d4536c3atagen2026-10-11 08:50 UTC1768
                gone += 1;
b85a0d4536c3atagen2026-10-11 08:50 UTC1769
            }
b85a0d4536c3atagen2026-10-11 08:50 UTC1770
        }
b85a0d4536c3atagen2026-10-11 08:50 UTC1771
        gone
b85a0d4536c3atagen2026-10-11 08:50 UTC1772
    }
b85a0d4536c3atagen2026-10-11 08:50 UTC1773
    let high = ruptures(90, 10).await;
b85a0d4536c3atagen2026-10-11 08:50 UTC1774
    let low = ruptures(30, 2).await;
b85a0d4536c3atagen2026-10-11 08:50 UTC1775
    assert!((0..=9).contains(&high), "high holds ruptured {high}/600");
b85a0d4536c3atagen2026-10-11 08:50 UTC1776
    assert!((0..=9).contains(&low), "low holds ruptured {low}/600");
b85a0d4536c3atagen2026-10-11 08:50 UTC1777
}
b85a0d4536c3atagen2026-10-11 08:50 UTC1778
b85a0d4536c3atagen2026-10-11 08:50 UTC1779
#[tokio::test]
b85a0d4536c3atagen2026-10-11 08:50 UTC1780
async fn the_kitchen_rolls_the_normal_tiers() {
b85a0d4536c3atagen2026-10-11 08:50 UTC1781
    async fn ruptures(eq: i32) -> u32 {
b85a0d4536c3atagen2026-10-11 08:50 UTC1782
        let mut gone = 0;
b85a0d4536c3atagen2026-10-11 08:50 UTC1783
        for seed in 0..600u64 {
b85a0d4536c3atagen2026-10-11 08:50 UTC1784
            let app = app_with(Floor::Bedroom, seed, |run| run.eq = eq);
b85a0d4536c3atagen2026-10-11 08:50 UTC1785
            let raid = send(&app, "POST", "/runs/cafebabe/kitchen", &[], "").await;
b85a0d4536c3atagen2026-10-11 08:50 UTC1786
            if raid.status() == StatusCode::INTERNAL_SERVER_ERROR {
b85a0d4536c3atagen2026-10-11 08:50 UTC1787
                gone += 1;
b85a0d4536c3atagen2026-10-11 08:50 UTC1788
            }
b85a0d4536c3atagen2026-10-11 08:50 UTC1789
        }
b85a0d4536c3atagen2026-10-11 08:50 UTC1790
        gone
b85a0d4536c3atagen2026-10-11 08:50 UTC1791
    }
b85a0d4536c3atagen2026-10-11 08:50 UTC1792
    let hot = ruptures(96).await;
b85a0d4536c3atagen2026-10-11 08:50 UTC1793
    let cool = ruptures(40).await;
b85a0d4536c3atagen2026-10-11 08:50 UTC1794
    assert!((40..=80).contains(&hot), "hot kitchen ruptured {hot}/600");
b85a0d4536c3atagen2026-10-11 08:50 UTC1795
    assert!((0..=9).contains(&cool), "cool kitchen ruptured {cool}/600");
b85a0d4536c3atagen2026-10-11 08:50 UTC1796
}
e807266c323batagen2026-10-12 09:46 UTC1797
e807266c323batagen2026-10-12 09:46 UTC1798
#[tokio::test]
e807266c323batagen2026-10-12 09:46 UTC1799
async fn the_thread_cannot_be_reread_before_anyone_mentions_it() {
e807266c323batagen2026-10-12 09:46 UTC1800
    let app = seeded_with(Floor::Bedroom, |run| run.warmed_up = true);
e807266c323batagen2026-10-12 09:46 UTC1801
    let read = send(&app, "POST", "/runs/cafebabe/re-read", &[], "").await;
e807266c323batagen2026-10-12 09:46 UTC1802
    assert_eq!(read.status(), StatusCode::NOT_FOUND);
e807266c323batagen2026-10-12 09:46 UTC1803
    let after = send(&app, "GET", "/runs/cafebabe", &[], "").await;
e807266c323batagen2026-10-12 09:46 UTC1804
    assert_eq!(header(&after, "x-floor"), "1");
e807266c323batagen2026-10-12 09:46 UTC1805
    assert!(!header(&after, "link").contains("/re-read"));
e807266c323batagen2026-10-12 09:46 UTC1806
}
e807266c323batagen2026-10-12 09:46 UTC1807
e807266c323batagen2026-10-12 09:46 UTC1808
#[tokio::test]
e807266c323batagen2026-10-12 09:46 UTC1809
async fn any_other_action_takes_the_melon_off_the_table() {
e807266c323batagen2026-10-12 09:46 UTC1810
    for (method, uri) in [
e807266c323batagen2026-10-12 09:46 UTC1811
        ("JELQ", "/runs/cafebabe?technique=warmup"),
e807266c323batagen2026-10-12 09:46 UTC1812
        ("POST", "/runs/cafebabe/drawer"),
e807266c323batagen2026-10-12 09:46 UTC1813
    ] {
e807266c323batagen2026-10-12 09:46 UTC1814
        let app = seeded_with(Floor::Bedroom, |run| {
e807266c323batagen2026-10-12 09:46 UTC1815
            run.eq = 95;
e807266c323batagen2026-10-12 09:46 UTC1816
            run.here.melon_offered = true;
e807266c323batagen2026-10-12 09:46 UTC1817
        });
e807266c323batagen2026-10-12 09:46 UTC1818
        send(&app, method, uri, &[], "").await;
e807266c323batagen2026-10-12 09:46 UTC1819
        let melon = send(&app, "POST", "/runs/cafebabe/kitchen/watermelon", &[], "").await;
e807266c323batagen2026-10-12 09:46 UTC1820
        assert_eq!(melon.status(), StatusCode::CONFLICT, "{method} {uri}");
e807266c323batagen2026-10-12 09:46 UTC1821
    }
e807266c323batagen2026-10-12 09:46 UTC1822
}
e807266c323batagen2026-10-12 09:46 UTC1823
e807266c323batagen2026-10-12 09:46 UTC1824
#[tokio::test]
e807266c323batagen2026-10-12 09:46 UTC1825
async fn a_refused_jelq_does_not_break_the_edging_streak() {
e807266c323batagen2026-10-12 09:46 UTC1826
    let app = seeded_with(Floor::ChangingRoom, |run| {
e807266c323batagen2026-10-12 09:46 UTC1827
        run.warmed_up = true;
e807266c323batagen2026-10-12 09:46 UTC1828
        run.eq = 45;
e807266c323batagen2026-10-12 09:46 UTC1829
        run.here.edges = 2;
e807266c323batagen2026-10-12 09:46 UTC1830
    });
e807266c323batagen2026-10-12 09:46 UTC1831
    let refused = send(
e807266c323batagen2026-10-12 09:46 UTC1832
        &app,
e807266c323batagen2026-10-12 09:46 UTC1833
        "JELQ",
e807266c323batagen2026-10-12 09:46 UTC1834
        "/runs/cafebabe?technique=dry",
e807266c323batagen2026-10-12 09:46 UTC1835
        &[("content-type", "application/baby-oil")],
e807266c323batagen2026-10-12 09:46 UTC1836
        "",
e807266c323batagen2026-10-12 09:46 UTC1837
    )
e807266c323batagen2026-10-12 09:46 UTC1838
    .await;
e807266c323batagen2026-10-12 09:46 UTC1839
    assert_eq!(refused.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
e807266c323batagen2026-10-12 09:46 UTC1840
    let third = send(&app, "JELQ", "/runs/cafebabe?technique=edge", &[], "").await;
e807266c323batagen2026-10-12 09:46 UTC1841
    assert_eq!(third.status(), StatusCode::LOOP_DETECTED);
e807266c323batagen2026-10-12 09:46 UTC1842
}
e807266c323batagen2026-10-12 09:46 UTC1843
e807266c323batagen2026-10-12 09:46 UTC1844
#[tokio::test]
e807266c323batagen2026-10-12 09:46 UTC1845
async fn an_interrupted_edge_keeps_no_gains() {
e807266c323batagen2026-10-12 09:46 UTC1846
    let app = seeded_with(Floor::ChangingRoom, |run| {
e807266c323batagen2026-10-12 09:46 UTC1847
        run.warmed_up = true;
e807266c323batagen2026-10-12 09:46 UTC1848
        run.eq = 10;
e807266c323batagen2026-10-12 09:46 UTC1849
    });
e807266c323batagen2026-10-12 09:46 UTC1850
    let before = send(&app, "GET", "/runs/cafebabe", &[], "").await;
e807266c323batagen2026-10-12 09:46 UTC1851
    let girth = header(&before, "x-girth").to_owned();
e807266c323batagen2026-10-12 09:46 UTC1852
    let edge = send(&app, "JELQ", "/runs/cafebabe?technique=edge", &[], "").await;
e807266c323batagen2026-10-12 09:46 UTC1853
    assert_eq!(edge.status(), StatusCode::NOT_FOUND);
e807266c323batagen2026-10-12 09:46 UTC1854
    assert_eq!(header(&edge, "x-girth"), girth);
e807266c323batagen2026-10-12 09:46 UTC1855
}
e807266c323batagen2026-10-12 09:46 UTC1856
e807266c323batagen2026-10-12 09:46 UTC1857
#[tokio::test]
e807266c323batagen2026-10-12 09:46 UTC1858
async fn rummaging_for_the_key_leaves_the_drawer_unsearched() {
e807266c323batagen2026-10-12 09:46 UTC1859
    let mut found = 0;
e807266c323batagen2026-10-12 09:46 UTC1860
    for seed in 0..20u64 {
e807266c323batagen2026-10-12 09:46 UTC1861
        let app = app_with(Floor::Hotel, seed, |run| {
e807266c323batagen2026-10-12 09:46 UTC1862
            run.here.wifi_accepted = true;
e807266c323batagen2026-10-12 09:46 UTC1863
            run.items.cage = true;
e807266c323batagen2026-10-12 09:46 UTC1864
        });
e807266c323batagen2026-10-12 09:46 UTC1865
        for _ in 0..30 {
e807266c323batagen2026-10-12 09:46 UTC1866
            let rummage = send(&app, "POST", "/runs/cafebabe/drawer", &[], "").await;
e807266c323batagen2026-10-12 09:46 UTC1867
            if rummage.status() == StatusCode::OK {
e807266c323batagen2026-10-12 09:46 UTC1868
                break;
e807266c323batagen2026-10-12 09:46 UTC1869
            }
e807266c323batagen2026-10-12 09:46 UTC1870
        }
e807266c323batagen2026-10-12 09:46 UTC1871
        let search = send(&app, "POST", "/runs/cafebabe/drawer", &[], "").await;
e807266c323batagen2026-10-12 09:46 UTC1872
        if search.status() == StatusCode::FOUND {
e807266c323batagen2026-10-12 09:46 UTC1873
            assert!(header(&search, "location").ends_with("/inventory/do-not-disturb-sign"));
e807266c323batagen2026-10-12 09:46 UTC1874
            found += 1;
e807266c323batagen2026-10-12 09:46 UTC1875
        }
e807266c323batagen2026-10-12 09:46 UTC1876
    }
e807266c323batagen2026-10-12 09:46 UTC1877
    assert!(
e807266c323batagen2026-10-12 09:46 UTC1878
        found >= 18,
e807266c323batagen2026-10-12 09:46 UTC1879
        "only {found} of 20 hotel drawers survived the rummage"
e807266c323batagen2026-10-12 09:46 UTC1880
    );
e807266c323batagen2026-10-12 09:46 UTC1881
}
e807266c323batagen2026-10-12 09:46 UTC1882
e807266c323batagen2026-10-12 09:46 UTC1883
#[tokio::test]
e807266c323batagen2026-10-12 09:46 UTC1884
async fn a_full_server_buries_the_dead_to_make_room() {
e807266c323batagen2026-10-12 09:46 UTC1885
    let app = crate::app(Config {
e807266c323batagen2026-10-12 09:46 UTC1886
        max_runs: 1,
e807266c323batagen2026-10-12 09:46 UTC1887
        ..Config::default()
e807266c323batagen2026-10-12 09:46 UTC1888
    });
e807266c323batagen2026-10-12 09:46 UTC1889
    let first = send(&app, "POST", "/runs", &[], "").await;
e807266c323batagen2026-10-12 09:46 UTC1890
    let location = header(&first, "location").to_owned();
e807266c323batagen2026-10-12 09:46 UTC1891
    let full = send(&app, "POST", "/runs", &[], "").await;
e807266c323batagen2026-10-12 09:46 UTC1892
    assert_eq!(full.status(), StatusCode::SERVICE_UNAVAILABLE);
e807266c323batagen2026-10-12 09:46 UTC1893
    send(&app, "DELETE", &location, &[], "").await;
e807266c323batagen2026-10-12 09:46 UTC1894
    let second = send(&app, "POST", "/runs", &[], "").await;
e807266c323batagen2026-10-12 09:46 UTC1895
    assert_eq!(second.status(), StatusCode::CREATED);
e807266c323batagen2026-10-12 09:46 UTC1896
}
a6caa2f99786atagen2026-10-12 10:38 UTC1897
a6caa2f99786atagen2026-10-12 10:38 UTC1898
#[tokio::test]
a6caa2f99786atagen2026-10-12 10:38 UTC1899
async fn the_towel_warms_once_until_the_kettle_boils() {
a6caa2f99786atagen2026-10-12 10:38 UTC1900
    let app = seeded_with(Floor::Bedroom, |_| {});
a6caa2f99786atagen2026-10-12 10:38 UTC1901
    let warm = send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
a6caa2f99786atagen2026-10-12 10:38 UTC1902
    assert_eq!(warm.status(), StatusCode::OK);
a6caa2f99786atagen2026-10-12 10:38 UTC1903
    let again = send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
a6caa2f99786atagen2026-10-12 10:38 UTC1904
    assert_eq!(again.status(), StatusCode::CONFLICT);
a6caa2f99786atagen2026-10-12 10:38 UTC1905
a6caa2f99786atagen2026-10-12 10:38 UTC1906
    let app = seeded_with(Floor::Bedroom, |run| {
a6caa2f99786atagen2026-10-12 10:38 UTC1907
        run.warmed_up = true;
a6caa2f99786atagen2026-10-12 10:38 UTC1908
        run.here.kettle_boiled = true;
a6caa2f99786atagen2026-10-12 10:38 UTC1909
    });
a6caa2f99786atagen2026-10-12 10:38 UTC1910
    let reheated = send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
a6caa2f99786atagen2026-10-12 10:38 UTC1911
    assert_eq!(reheated.status(), StatusCode::OK);
a6caa2f99786atagen2026-10-12 10:38 UTC1912
    let cold = send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
a6caa2f99786atagen2026-10-12 10:38 UTC1913
    assert_eq!(cold.status(), StatusCode::CONFLICT);
a6caa2f99786atagen2026-10-12 10:38 UTC1914
}
a6caa2f99786atagen2026-10-12 10:38 UTC1915
a6caa2f99786atagen2026-10-12 10:38 UTC1916
#[tokio::test]
a6caa2f99786atagen2026-10-12 10:38 UTC1917
async fn the_refractory_period_is_the_next_hold() {
a6caa2f99786atagen2026-10-12 10:38 UTC1918
    let app = seeded_with(Floor::Hotel, |run| {
a6caa2f99786atagen2026-10-12 10:38 UTC1919
        run.here.wifi_accepted = true;
a6caa2f99786atagen2026-10-12 10:38 UTC1920
        run.warmed_up = true;
a6caa2f99786atagen2026-10-12 10:38 UTC1921
        run.eq = 60;
a6caa2f99786atagen2026-10-12 10:38 UTC1922
        run.stamina = 0;
a6caa2f99786atagen2026-10-12 10:38 UTC1923
        run.exhausted = true;
a6caa2f99786atagen2026-10-12 10:38 UTC1924
    });
a6caa2f99786atagen2026-10-12 10:38 UTC1925
    let tired = send(&app, "JELQ", "/runs/cafebabe?technique=edge", &[], "").await;
a6caa2f99786atagen2026-10-12 10:38 UTC1926
    assert_eq!(tired.status(), StatusCode::TOO_MANY_REQUESTS);
a6caa2f99786atagen2026-10-12 10:38 UTC1927
    let hold = send(&app, "HOLD", "/runs/cafebabe", &[], "").await;
a6caa2f99786atagen2026-10-12 10:38 UTC1928
    assert_eq!(hold.status(), StatusCode::OK);
a6caa2f99786atagen2026-10-12 10:38 UTC1929
    assert_eq!(header(&hold, "x-stamina"), "10/10");
a6caa2f99786atagen2026-10-12 10:38 UTC1930
    let cold = send(&app, "JELQ", "/runs/cafebabe?technique=edge", &[], "").await;
a6caa2f99786atagen2026-10-12 10:38 UTC1931
    assert_eq!(cold.status(), StatusCode::PRECONDITION_FAILED);
a6caa2f99786atagen2026-10-12 10:38 UTC1932
}
a6caa2f99786atagen2026-10-12 10:38 UTC1933
a6caa2f99786atagen2026-10-12 10:38 UTC1934
#[tokio::test]
a6caa2f99786atagen2026-10-12 10:38 UTC1935
async fn friction_rises_with_vigour_and_edging_chafes() {
a6caa2f99786atagen2026-10-12 10:38 UTC1936
    let spit = [("content-type", "text/plain; charset=spit")];
a6caa2f99786atagen2026-10-12 10:38 UTC1937
    for (intensity, friction) in [(5, "1/10"), (7, "2/10"), (10, "3/10")] {
a6caa2f99786atagen2026-10-12 10:38 UTC1938
        let app = seeded_with(Floor::Hotel, |run| {
a6caa2f99786atagen2026-10-12 10:38 UTC1939
            run.here.wifi_accepted = true;
a6caa2f99786atagen2026-10-12 10:38 UTC1940
            run.warmed_up = true;
a6caa2f99786atagen2026-10-12 10:38 UTC1941
            run.eq = 55;
058650ec79d6atagen2026-10-12 10:49 UTC1942
            run.foreskin = Some(0.5);
a6caa2f99786atagen2026-10-12 10:38 UTC1943
        });
a6caa2f99786atagen2026-10-12 10:38 UTC1944
        let uri = format!("/runs/cafebabe?technique=wet&intensity={intensity}");
a6caa2f99786atagen2026-10-12 10:38 UTC1945
        let jelq = send(&app, "JELQ", &uri, &spit, "").await;
a6caa2f99786atagen2026-10-12 10:38 UTC1946
        assert_eq!(
a6caa2f99786atagen2026-10-12 10:38 UTC1947
            header(&jelq, "x-friction"),
a6caa2f99786atagen2026-10-12 10:38 UTC1948
            friction,
a6caa2f99786atagen2026-10-12 10:38 UTC1949
            "intensity {intensity}"
a6caa2f99786atagen2026-10-12 10:38 UTC1950
        );
a6caa2f99786atagen2026-10-12 10:38 UTC1951
    }
a6caa2f99786atagen2026-10-12 10:38 UTC1952
    let app = seeded_with(Floor::Hotel, |run| {
a6caa2f99786atagen2026-10-12 10:38 UTC1953
        run.here.wifi_accepted = true;
a6caa2f99786atagen2026-10-12 10:38 UTC1954
        run.warmed_up = true;
a6caa2f99786atagen2026-10-12 10:38 UTC1955
        run.eq = 55;
058650ec79d6atagen2026-10-12 10:49 UTC1956
        run.foreskin = Some(0.5);
a6caa2f99786atagen2026-10-12 10:38 UTC1957
    });
a6caa2f99786atagen2026-10-12 10:38 UTC1958
    let edge = send(&app, "JELQ", "/runs/cafebabe?technique=edge", &[], "").await;
a6caa2f99786atagen2026-10-12 10:38 UTC1959
    assert_eq!(header(&edge, "x-friction"), "2/10");
a6caa2f99786atagen2026-10-12 10:38 UTC1960
}
058650ec79d6atagen2026-10-12 10:49 UTC1961
058650ec79d6atagen2026-10-12 10:49 UTC1962
#[tokio::test]
058650ec79d6atagen2026-10-12 10:49 UTC1963
async fn the_body_changes_how_the_skin_takes_it() {
058650ec79d6atagen2026-10-12 10:49 UTC1964
    let spit = [("content-type", "text/plain; charset=spit")];
058650ec79d6atagen2026-10-12 10:49 UTC1965
    for (foreskin, girth, friction) in [(0.5, 4.5, "1/10"), (1.2, 4.5, "0/10"), (0.5, 5.8, "2/10")]
058650ec79d6atagen2026-10-12 10:49 UTC1966
    {
058650ec79d6atagen2026-10-12 10:49 UTC1967
        let app = seeded_with(Floor::Hotel, |run| {
058650ec79d6atagen2026-10-12 10:49 UTC1968
            run.here.wifi_accepted = true;
058650ec79d6atagen2026-10-12 10:49 UTC1969
            run.warmed_up = true;
058650ec79d6atagen2026-10-12 10:49 UTC1970
            run.eq = 55;
058650ec79d6atagen2026-10-12 10:49 UTC1971
            run.foreskin = Some(foreskin);
058650ec79d6atagen2026-10-12 10:49 UTC1972
            run.girth = girth;
058650ec79d6atagen2026-10-12 10:49 UTC1973
        });
058650ec79d6atagen2026-10-12 10:49 UTC1974
        let jelq = send(&app, "JELQ", "/runs/cafebabe?technique=wet", &spit, "").await;
058650ec79d6atagen2026-10-12 10:49 UTC1975
        assert_eq!(header(&jelq, "x-friction"), friction, "{foreskin} {girth}");
058650ec79d6atagen2026-10-12 10:49 UTC1976
    }
058650ec79d6atagen2026-10-12 10:49 UTC1977
}
058650ec79d6atagen2026-10-12 10:49 UTC1978
058650ec79d6atagen2026-10-12 10:49 UTC1979
#[tokio::test]
058650ec79d6atagen2026-10-12 10:49 UTC1980
async fn a_thick_girth_outgrows_the_underhand_grip() {
058650ec79d6atagen2026-10-12 10:49 UTC1981
    let app = seeded_with(Floor::Bedroom, |run| run.girth = 5.8);
058650ec79d6atagen2026-10-12 10:49 UTC1982
    let grip = send(
058650ec79d6atagen2026-10-12 10:49 UTC1983
        &app,
058650ec79d6atagen2026-10-12 10:49 UTC1984
        "GET",
058650ec79d6atagen2026-10-12 10:49 UTC1985
        "/runs/cafebabe/grip",
058650ec79d6atagen2026-10-12 10:49 UTC1986
        &[("upgrade", "underhand"), ("connection", "upgrade")],
058650ec79d6atagen2026-10-12 10:49 UTC1987
        "",
058650ec79d6atagen2026-10-12 10:49 UTC1988
    )
058650ec79d6atagen2026-10-12 10:49 UTC1989
    .await;
058650ec79d6atagen2026-10-12 10:49 UTC1990
    assert_eq!(grip.status(), StatusCode::PAYLOAD_TOO_LARGE);
058650ec79d6atagen2026-10-12 10:49 UTC1991
}
058650ec79d6atagen2026-10-12 10:49 UTC1992
058650ec79d6atagen2026-10-12 10:49 UTC1993
#[tokio::test]
058650ec79d6atagen2026-10-12 10:49 UTC1994
async fn respectable_girth_waves_off_the_gym_staff() {
058650ec79d6atagen2026-10-12 10:49 UTC1995
    for (girth, status) in [
058650ec79d6atagen2026-10-12 10:49 UTC1996
        (4.5, StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS),
058650ec79d6atagen2026-10-12 10:49 UTC1997
        (5.2, StatusCode::OK),
058650ec79d6atagen2026-10-12 10:49 UTC1998
    ] {
058650ec79d6atagen2026-10-12 10:49 UTC1999
        let app = seeded_with(Floor::ChangingRoom, |run| {
058650ec79d6atagen2026-10-12 10:49 UTC2000
            run.warmed_up = true;
058650ec79d6atagen2026-10-12 10:49 UTC2001
            run.eq = 45;
058650ec79d6atagen2026-10-12 10:49 UTC2002
            run.girth = girth;
058650ec79d6atagen2026-10-12 10:49 UTC2003
            run.suspicion = SUSPICION_LIMIT;
058650ec79d6atagen2026-10-12 10:49 UTC2004
        });
058650ec79d6atagen2026-10-12 10:49 UTC2005
        let edge = send(&app, "JELQ", "/runs/cafebabe?technique=edge", &[], "").await;
058650ec79d6atagen2026-10-12 10:49 UTC2006
        if girth > 5.0 {
058650ec79d6atagen2026-10-12 10:49 UTC2007
            assert_ne!(edge.status(), StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS);
058650ec79d6atagen2026-10-12 10:49 UTC2008
            assert!(header(&edge, "warning").contains("gym staff"));
058650ec79d6atagen2026-10-12 10:49 UTC2009
        } else {
058650ec79d6atagen2026-10-12 10:49 UTC2010
            assert_eq!(edge.status(), status);
058650ec79d6atagen2026-10-12 10:49 UTC2011
        }
058650ec79d6atagen2026-10-12 10:49 UTC2012
    }
058650ec79d6atagen2026-10-12 10:49 UTC2013
}
058650ec79d6atagen2026-10-12 10:49 UTC2014
058650ec79d6atagen2026-10-12 10:49 UTC2015
#[tokio::test]
058650ec79d6atagen2026-10-12 10:49 UTC2016
async fn kev_is_the_bar_in_the_changing_room() {
058650ec79d6atagen2026-10-12 10:49 UTC2017
    for (length, shamed) in [(5.0, true), (7.0, false)] {
058650ec79d6atagen2026-10-12 10:49 UTC2018
        let app = seeded_with(Floor::Bathroom, |run| {
058650ec79d6atagen2026-10-12 10:49 UTC2019
            run.length = length;
058650ec79d6atagen2026-10-12 10:49 UTC2020
            run.here.turns = 6;
058650ec79d6atagen2026-10-12 10:49 UTC2021
        });
058650ec79d6atagen2026-10-12 10:49 UTC2022
        let left = send(&app, "POST", "/runs/cafebabe/floor", &[], "").await;
058650ec79d6atagen2026-10-12 10:49 UTC2023
        assert_eq!(header(&left, "x-floor"), "3");
058650ec79d6atagen2026-10-12 10:49 UTC2024
        let warning = left
058650ec79d6atagen2026-10-12 10:49 UTC2025
            .headers()
058650ec79d6atagen2026-10-12 10:49 UTC2026
            .get("warning")
058650ec79d6atagen2026-10-12 10:49 UTC2027
            .map(|w| w.to_str().unwrap().to_owned());
058650ec79d6atagen2026-10-12 10:49 UTC2028
        assert_eq!(
058650ec79d6atagen2026-10-12 10:49 UTC2029
            warning.is_some_and(|w| w.contains("Kev")),
058650ec79d6atagen2026-10-12 10:49 UTC2030
            shamed,
058650ec79d6atagen2026-10-12 10:49 UTC2031
            "length {length}"
058650ec79d6atagen2026-10-12 10:49 UTC2032
        );
058650ec79d6atagen2026-10-12 10:49 UTC2033
    }
058650ec79d6atagen2026-10-12 10:49 UTC2034
}
058650ec79d6atagen2026-10-12 10:49 UTC2035
058650ec79d6atagen2026-10-12 10:49 UTC2036
#[tokio::test]
058650ec79d6atagen2026-10-12 10:49 UTC2037
async fn the_forum_respects_volume() {
058650ec79d6atagen2026-10-12 10:49 UTC2038
    let state = Arc::new(AppState::default());
058650ec79d6atagen2026-10-12 10:49 UTC2039
    for (id, length, girth) in [("slim", 7.0, 4.0), ("stout", 5.0, 5.5)] {
058650ec79d6atagen2026-10-12 10:49 UTC2040
        let mut run = Run::new(id.into(), id.into(), 1);
058650ec79d6atagen2026-10-12 10:49 UTC2041
        run.length = length;
058650ec79d6atagen2026-10-12 10:49 UTC2042
        run.girth = girth;
058650ec79d6atagen2026-10-12 10:49 UTC2043
        state.insert(run);
058650ec79d6atagen2026-10-12 10:49 UTC2044
    }
058650ec79d6atagen2026-10-12 10:49 UTC2045
    let app = router(state);
058650ec79d6atagen2026-10-12 10:49 UTC2046
    let board = json(
058650ec79d6atagen2026-10-12 10:49 UTC2047
        send(
058650ec79d6atagen2026-10-12 10:49 UTC2048
            &app,
058650ec79d6atagen2026-10-12 10:49 UTC2049
            "GET",
058650ec79d6atagen2026-10-12 10:49 UTC2050
            "/leaderboard?sort=volume&seeded=include",
058650ec79d6atagen2026-10-12 10:49 UTC2051
            &[],
058650ec79d6atagen2026-10-12 10:49 UTC2052
            "",
058650ec79d6atagen2026-10-12 10:49 UTC2053
        )
058650ec79d6atagen2026-10-12 10:49 UTC2054
        .await,
058650ec79d6atagen2026-10-12 10:49 UTC2055
    )
058650ec79d6atagen2026-10-12 10:49 UTC2056
    .await;
058650ec79d6atagen2026-10-12 10:49 UTC2057
    assert_eq!(board["entries"][0]["public_id"], "stout");
058650ec79d6atagen2026-10-12 10:49 UTC2058
    assert!(board["entries"][0]["volume"].as_f64().unwrap() > 0.0);
058650ec79d6atagen2026-10-12 10:49 UTC2059
}
159f0998263batagen2026-10-12 11:01 UTC2060
159f0998263batagen2026-10-12 11:01 UTC2061
#[tokio::test]
159f0998263batagen2026-10-12 11:01 UTC2062
async fn the_dead_outlive_the_server() {
159f0998263batagen2026-10-12 11:01 UTC2063
    let path = std::env::temp_dir().join(format!("jelqd-{}.sqlite", std::process::id()));
159f0998263batagen2026-10-12 11:01 UTC2064
    let config = || Config {
159f0998263batagen2026-10-12 11:01 UTC2065
        db: Some(path.clone()),
159f0998263batagen2026-10-12 11:01 UTC2066
        ..Config::default()
159f0998263batagen2026-10-12 11:01 UTC2067
    };
159f0998263batagen2026-10-12 11:01 UTC2068
    let first = crate::app(config());
159f0998263batagen2026-10-12 11:01 UTC2069
    let created = send(&first, "POST", "/runs", &[], "").await;
159f0998263batagen2026-10-12 11:01 UTC2070
    let location = header(&created, "location").to_owned();
159f0998263batagen2026-10-12 11:01 UTC2071
    send(&first, "DELETE", &location, &[], "").await;
159f0998263batagen2026-10-12 11:01 UTC2072
    drop(first);
159f0998263batagen2026-10-12 11:01 UTC2073
159f0998263batagen2026-10-12 11:01 UTC2074
    let second = crate::app(config());
159f0998263batagen2026-10-12 11:01 UTC2075
    let gone = send(&second, "GET", &location, &[], "").await;
159f0998263batagen2026-10-12 11:01 UTC2076
    assert_eq!(gone.status(), StatusCode::GONE);
159f0998263batagen2026-10-12 11:01 UTC2077
    let graveyard = json(send(&second, "GET", "/graveyard", &[], "").await).await;
159f0998263batagen2026-10-12 11:01 UTC2078
    assert_eq!(graveyard["runs"].as_array().unwrap().len(), 1);
159f0998263batagen2026-10-12 11:01 UTC2079
    let board = json(send(&second, "GET", "/leaderboard", &[], "").await).await;
159f0998263batagen2026-10-12 11:01 UTC2080
    assert_eq!(board["entries"][0]["status"], "gone");
159f0998263batagen2026-10-12 11:01 UTC2081
    for suffix in ["", "-wal", "-shm"] {
159f0998263batagen2026-10-12 11:01 UTC2082
        let _ = std::fs::remove_file(format!("{}{suffix}", path.display()));
159f0998263batagen2026-10-12 11:01 UTC2083
    }
159f0998263batagen2026-10-12 11:01 UTC2084
}
9374b3933189atagen2026-10-12 11:15 UTC2085
9374b3933189atagen2026-10-12 11:15 UTC2086
#[tokio::test]
9374b3933189atagen2026-10-12 11:15 UTC2087
async fn pump_bro_charges_the_thin_more_for_girth() {
9374b3933189atagen2026-10-12 11:15 UTC2088
    for (girth, ring, oil) in [(4.5, 11, 4), (5.2, 8, 4)] {
9374b3933189atagen2026-10-12 11:15 UTC2089
        let app = seeded_with(Floor::ChangingRoom, |run| run.girth = girth);
9374b3933189atagen2026-10-12 11:15 UTC2090
        let shop = json(send(&app, "GET", "/runs/cafebabe/shop", &[], "").await).await;
9374b3933189atagen2026-10-12 11:15 UTC2091
        let price = |slug: &str| {
9374b3933189atagen2026-10-12 11:15 UTC2092
            shop["stock"]
9374b3933189atagen2026-10-12 11:15 UTC2093
                .as_array()
9374b3933189atagen2026-10-12 11:15 UTC2094
                .unwrap()
9374b3933189atagen2026-10-12 11:15 UTC2095
                .iter()
9374b3933189atagen2026-10-12 11:15 UTC2096
                .find(|item| item["item"] == slug)
9374b3933189atagen2026-10-12 11:15 UTC2097
                .unwrap()["price"]
9374b3933189atagen2026-10-12 11:15 UTC2098
                .as_u64()
9374b3933189atagen2026-10-12 11:15 UTC2099
                .unwrap()
9374b3933189atagen2026-10-12 11:15 UTC2100
        };
9374b3933189atagen2026-10-12 11:15 UTC2101
        assert_eq!(price("cock-ring"), ring, "girth {girth}");
9374b3933189atagen2026-10-12 11:15 UTC2102
        assert_eq!(price("baby-oil"), oil, "girth {girth}");
9374b3933189atagen2026-10-12 11:15 UTC2103
    }
9374b3933189atagen2026-10-12 11:15 UTC2104
}
9374b3933189atagen2026-10-12 11:15 UTC2105
9374b3933189atagen2026-10-12 11:15 UTC2106
#[tokio::test]
9374b3933189atagen2026-10-12 11:15 UTC2107
async fn a_long_run_fills_the_jeans_before_a_thick_one() {
9374b3933189atagen2026-10-12 11:15 UTC2108
    let app = seeded_with(Floor::Bedroom, |run| {
9374b3933189atagen2026-10-12 11:15 UTC2109
        run.warmed_up = true;
9374b3933189atagen2026-10-12 11:15 UTC2110
        run.items.extender = true;
9374b3933189atagen2026-10-12 11:15 UTC2111
        run.girth = 4.8;
9374b3933189atagen2026-10-12 11:15 UTC2112
        run.length = 7.5;
9374b3933189atagen2026-10-12 11:15 UTC2113
    });
9374b3933189atagen2026-10-12 11:15 UTC2114
    let lube = [("content-type", "application/baby-oil")];
9374b3933189atagen2026-10-12 11:15 UTC2115
    let full = send(&app, "JELQ", "/runs/cafebabe?technique=wet", &lube, "").await;
9374b3933189atagen2026-10-12 11:15 UTC2116
    assert_eq!(full.status(), StatusCode::INSUFFICIENT_STORAGE);
9374b3933189atagen2026-10-12 11:15 UTC2117
}