v3 : relecture de cd7e683 — coquilles, objections et retraits
ci/woodpecker/push/app Pipeline was successful
ci/woodpecker/push/maquette Pipeline was successful

- 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:
Yvv
2026-10-31 10:00:29 +01:00
co-authored by Claude Opus 5.5
parent 033b331228
commit 4d4adcc281
8 changed files with 220 additions and 20 deletions
+65 -3
View File
@@ -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");
}
}