forked from yvv/decision
v3 : relecture de cd7e683 — coquilles, objections et retraits
- Le revirement de l'auteur sur une coquille (reprendre une correction appliquée, valider une correction refusée) ne s'écrit que sur une version encore ouverte : Settle porte while_open ; et la garde de statut de write_text marche enfin (?9 restait NULL, le statut est le 8e paramètre). - 0008 lève les signalements debout dont un côté est déjà retiré. - Une seule objection debout par personne et par paire (index unique partiel) ; envoyée plusieurs fois à la fois, les autres reçoivent objection_exists. - Un retrait relance l'adoption : la version que l'objection retenait entre aussitôt. Tests vus rouges d'abord. Boucle : 230 tests Rust, svelte-check 0, 55 vitest, e2e 35 / 35. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -350,7 +350,7 @@ pub async fn raise(
|
||||
return Err(AppError::Conflict("objection_exists"));
|
||||
}
|
||||
let oid = util::id();
|
||||
sqlx::query(
|
||||
let inserted = sqlx::query(
|
||||
"INSERT INTO signals (id, proposal_id, target_proposal_id, reason, author_id, created_at, kind, held_id, phase, cycle)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'author', 1)",
|
||||
)
|
||||
@@ -363,7 +363,12 @@ pub async fn raise(
|
||||
.bind(kind.as_str())
|
||||
.bind(&held)
|
||||
.execute(&state.db)
|
||||
.await?;
|
||||
.await;
|
||||
match inserted {
|
||||
// The same one sent at the same instant: it stands already (`signals_one_standing`).
|
||||
Err(sqlx::Error::Database(e)) if e.is_unique_violation() => return Err(AppError::Conflict("objection_exists")),
|
||||
other => other?,
|
||||
};
|
||||
if let Some(author) = author_of(&state, &held).await?.filter(|a| *a != user.id) {
|
||||
messages::notify(&state, &author, &st.doc.id, Topic::Objection, &oid, "objection_raised", 1, None).await?;
|
||||
}
|
||||
|
||||
@@ -445,7 +445,7 @@ pub async fn withdraw(
|
||||
User(user): User,
|
||||
Path(id): Path<String>,
|
||||
) -> AppResult<Json<serde_json::Value>> {
|
||||
let (p, _) = guarded(&state, &user, &id).await?;
|
||||
let (p, st) = guarded(&state, &user, &id).await?;
|
||||
if p.author_id.as_deref() != Some(user.id.as_str()) {
|
||||
return Err(AppError::Forbidden);
|
||||
}
|
||||
@@ -461,6 +461,8 @@ pub async fn withdraw(
|
||||
// Nothing left to object to, nor to correct.
|
||||
objections::on_withdrawn(&state, &id, &user.id).await?;
|
||||
typos::on_text_changed(&state, &id).await?;
|
||||
// A version its objection held back may enter now, with its votes (relecture of cd7e683).
|
||||
adoption::run(&state, &st.doc.id).await?;
|
||||
Ok(Json(json!({ "status": "withdrawn" })))
|
||||
}
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ async fn write_text(
|
||||
let done = sqlx::query(
|
||||
"UPDATE proposals SET title = ?, body = ?, updated_at = ?
|
||||
WHERE id = ? AND revision = ? AND title = ? AND body = ? AND status IN ('open', 'in_force')
|
||||
AND (?9 IS NULL OR status = ?9)",
|
||||
AND (?8 IS NULL OR status = ?8)",
|
||||
)
|
||||
.bind(to.0)
|
||||
.bind(to.1)
|
||||
@@ -277,6 +277,8 @@ async fn settle(state: &AppState, t: &TypoRow, s: Settle, by: &str, now: &str) -
|
||||
if claimed.rows_affected() != 1 {
|
||||
return Ok(Err("settled"));
|
||||
}
|
||||
// The author's change of mind lands only on a version still open (relecture of cd7e683).
|
||||
let only = s.while_open.then_some("open");
|
||||
let written = match s.text {
|
||||
TextChange::Write => {
|
||||
write_text(
|
||||
@@ -284,7 +286,7 @@ async fn settle(state: &AppState, t: &TypoRow, s: Settle, by: &str, now: &str) -
|
||||
&t.proposal_id,
|
||||
t.base(),
|
||||
(&t.title, &t.body),
|
||||
None,
|
||||
only,
|
||||
&t.writer_id,
|
||||
Some((&t.id, by)),
|
||||
"typo",
|
||||
@@ -298,7 +300,7 @@ async fn settle(state: &AppState, t: &TypoRow, s: Settle, by: &str, now: &str) -
|
||||
&t.proposal_id,
|
||||
t.fixed(),
|
||||
(&t.base_title, &t.base_body),
|
||||
None,
|
||||
only,
|
||||
by,
|
||||
Some((&t.id, by)),
|
||||
"revert",
|
||||
@@ -475,3 +477,63 @@ pub async fn for_message(
|
||||
v["gestures"] = json!(gestures);
|
||||
Ok(Some((v, gestures)))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{Config, open_db, seed};
|
||||
|
||||
/// Relecture of cd7e683 (M1): the author takes back a fix applied, read while her
|
||||
/// version was open; a vote puts it in force in between. The text in force stays.
|
||||
#[tokio::test]
|
||||
async fn a_change_of_mind_never_writes_over_a_text_just_in_force() {
|
||||
let db = open_db("sqlite::memory:").await.expect("db");
|
||||
let state = AppState::new(Config::for_tests(), db).await.expect("state");
|
||||
seed::ensure(&state).await.expect("seed");
|
||||
let now = "2026-10-31T00:00:00Z";
|
||||
let (id, doc, revision, title, body): (String, String, i64, String, String) = sqlx::query_as(
|
||||
"SELECT p.id, u.doc_id, p.revision, p.title, p.body FROM proposals p JOIN units u ON u.id = p.unit_id
|
||||
WHERE p.status = 'in_force' AND p.kind = 'text' LIMIT 1",
|
||||
)
|
||||
.fetch_one(&state.db)
|
||||
.await
|
||||
.expect("a version in force");
|
||||
sqlx::query(
|
||||
"INSERT INTO identities (id, kind, display_name, created_at) VALUES ('dev:aude', 'dev', 'Aude', ?)",
|
||||
)
|
||||
.bind(now)
|
||||
.execute(&state.db)
|
||||
.await
|
||||
.expect("identity");
|
||||
// Its text is the one a fix wrote over « Un texte d'avant ».
|
||||
sqlx::query(
|
||||
"INSERT INTO typos (id, proposal_id, doc_id, writer_id, answerer_id, revision, base_title, base_body,
|
||||
title, body, status, created_at, due_at)
|
||||
VALUES ('t1', ?, ?, 'dev:aude', 'dev:aude', ?, ?, 'Un texte d''avant', ?, ?, 'applied', ?, ?)",
|
||||
)
|
||||
.bind(&id)
|
||||
.bind(&doc)
|
||||
.bind(revision)
|
||||
.bind(&title)
|
||||
.bind(&title)
|
||||
.bind(&body)
|
||||
.bind(now)
|
||||
.bind(now)
|
||||
.execute(&state.db)
|
||||
.await
|
||||
.expect("typo");
|
||||
let t = load(&state, "t1").await.expect("load");
|
||||
// What `answer` decided when it read the version, still open.
|
||||
let s = correction::answer(TypoStatus::Applied, Answer::Refuse, false).expect("refuse");
|
||||
assert_eq!(settle(&state, &t, s, "dev:aude", now).await.expect("settle"), Err("changed"));
|
||||
let kept: String = sqlx::query_scalar("SELECT body FROM proposals WHERE id = ?")
|
||||
.bind(&id)
|
||||
.fetch_one(&state.db)
|
||||
.await
|
||||
.expect("body");
|
||||
assert_eq!(kept, body, "the text in force stays");
|
||||
let status: String =
|
||||
sqlx::query_scalar("SELECT status FROM typos WHERE id = 't1'").fetch_one(&state.db).await.expect("status");
|
||||
assert_eq!(status, "applied", "nothing settled");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user