v3 : troisième relecture — une coquille n'inverse plus un sens ; retour à la version précédente
- Mot mal tapé = un seul glissement (une lettre ajoutée, retirée, ou deux voisines inversées), jamais au début du mot (préfixes : valide/invalide, limitée/illimitée), jamais une lettre remplacée (deux/dix, majorité/minorité), jamais sur un mot de négation ou de quantité, mots de 4 lettres au moins. - Parenthèses, crochets, « / » et tout ce qui est entre accents graves comptent comme le sens (ceil(N^(1/5)) → ceil(N^1/5) n'est plus une coquille). - Caractères invisibles (inversion du sens d'écriture, largeur nulle, BOM) refusés dans les textes, titres, traductions et motifs de signalement. - Déploiement : la version en service est gardée (image « previous ») ; un témoin marque le déploiement en cours ; « retablir » relance la maquette au premier déploiement, la version précédente ensuite ; la liste des conteneurs retirés n'est plus vidée par un pipeline annulé. - Tests : 59 Rust (dont 14 scénarios d'API, avec deux → dix sur E2 en vigueur et l'inversion bidi), e2e 12 / 12. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -59,6 +59,9 @@ async fn guarded(state: &AppState, user: &IdentityRow, proposal_id: &str) -> App
|
||||
}
|
||||
|
||||
fn check_text(title: &str, body: &str, needs_body: bool) -> AppResult<()> {
|
||||
if edit::has_hidden_controls(title) || edit::has_hidden_controls(body) {
|
||||
return Err(AppError::Invalid("hidden_characters"));
|
||||
}
|
||||
if title.trim().is_empty() || title.chars().count() > MAX_TITLE {
|
||||
return Err(AppError::Invalid("title"));
|
||||
}
|
||||
@@ -647,7 +650,7 @@ pub async fn signal(
|
||||
) -> AppResult<Json<serde_json::Value>> {
|
||||
let (p, st) = guarded(&state, &user, &id).await?;
|
||||
let reason = b.reason.trim();
|
||||
if reason.chars().count() < MIN_REASON || reason.chars().count() > MAX_REASON {
|
||||
if reason.chars().count() < MIN_REASON || reason.chars().count() > MAX_REASON || edit::has_hidden_controls(reason) {
|
||||
return Err(AppError::Invalid("reason"));
|
||||
}
|
||||
if b.target_id == id {
|
||||
|
||||
@@ -230,6 +230,27 @@ async fn editing_is_not_voting() {
|
||||
.await;
|
||||
assert_eq!(s, StatusCode::CONFLICT, "{v}");
|
||||
assert_eq!(v["error"], "substantive_not_author");
|
||||
// One word turned around in a text in force, and an invisible bidi override: refused.
|
||||
let e2 = unit(&doc, "E2")["inForce"].clone();
|
||||
let e2_id = e2["id"].as_str().expect("id").to_owned();
|
||||
let body = e2["body"].as_str().expect("body").to_owned();
|
||||
assert!(body.contains("ces deux présents"));
|
||||
let (s, v) = b
|
||||
.call(
|
||||
"PATCH",
|
||||
&format!("/api/proposals/{e2_id}"),
|
||||
Some(json!({ "title": e2["title"], "body": body.replacen("deux", "dix", 1) })),
|
||||
)
|
||||
.await;
|
||||
assert_eq!((s, v["error"].as_str()), (StatusCode::CONFLICT, Some("substantive_in_force")));
|
||||
let (s, v) = b
|
||||
.call(
|
||||
"PATCH",
|
||||
&format!("/api/proposals/{e2_id}"),
|
||||
Some(json!({ "title": e2["title"], "body": format!("\u{202e}{body}") })),
|
||||
)
|
||||
.await;
|
||||
assert_eq!((s, v["error"].as_str()), (StatusCode::BAD_REQUEST, Some("hidden_characters")));
|
||||
|
||||
// A member proposal: typo keeps votes, substance asks to reconfirm.
|
||||
let uid = unit(&doc, "E2")["id"].as_str().expect("u").to_owned();
|
||||
|
||||
Reference in New Issue
Block a user