mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-16 19:59:56 +02:00
Lot 38 : l'outil criteria du mode Code exécuté par la façade
L'outil par lequel le modèle tient les critères d'acceptation de sa tâche (poser, marquer faits, lister, effacer) partait au Go, qui renvoyait la liste à la façade comme delta. Il s'exécute maintenant dans le tour de la façade (chat/crittool.rs, port de toolCriteria) : mêmes messages, ids en nombre ou en chaîne, vingt critères au plus, `passed` réservé à la passe de vérification, rangement scellé (chat/crit:<id>), liste poussée à l'écran à chaque changement par un nouveau champ `deltas` de l'hôte du tour. Les critères sont rangés sous la discussion du tour (le Go prenait la discussion active : elles ne diffèrent que si l'utilisateur en change pendant la génération). see_image reste au Go (préparation de l'image). Parité : journal de 22 appels tiré du Go (parity_criteria_test.go → tests/parity_tools/criteria.json), rejoué par tests/tools.rs ; le contrat du mode Code de tests/lot6.rs est désormais vérifié par la façade seule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
This commit is contained in:
12 files changed
+1119
-21
No files matched your search
@@ -0,0 +1,77 @@
|
||||
# Lot 38 — L'outil criteria du mode Code dans la façade
|
||||
|
||||
## Contexte
|
||||
|
||||
En mode Code, le modèle tient les critères d'acceptation de sa tâche avec
|
||||
l'outil `criteria` :
|
||||
|
||||
- les poser ;
|
||||
- les marquer faits ;
|
||||
- les lister ;
|
||||
- les effacer.
|
||||
|
||||
La passe de vérification les marque ensuite `passed` ou `failed`.
|
||||
|
||||
Depuis le lot 19, la façade sert déjà les critères de l'écran
|
||||
(`/api/chat/criteria`, `chat/crit:<id>` scellés). La passe de vérification
|
||||
les relit depuis le lot 7. Mais l'outil lui-même partait au Go
|
||||
(`/internal/v1/tool/exec`), qui le renvoyait à la façade comme delta
|
||||
(`critNotify` → `/internal/facade/v1/delta`).
|
||||
|
||||
## Le lot
|
||||
|
||||
`chat/crittool.rs` porte `toolCriteria`, mêmes messages au caractère près :
|
||||
|
||||
- **list** : action vide ou d'un autre type comprise ;
|
||||
- **add** :
|
||||
- textes vides ou d'un autre type ignorés ;
|
||||
- ids repris après le plus grand ;
|
||||
- vingt critères au plus ;
|
||||
- **set** :
|
||||
- id numérique (tronqué) ou en chaîne (« "2" », « "#2" », « +1 ») ;
|
||||
- statut libre, note rognée ;
|
||||
- `passed` refusé hors de la passe de vérification ;
|
||||
- **clear**.
|
||||
|
||||
Les règles de rangement et d'affichage :
|
||||
|
||||
- une liste vide supprime la clé ; une erreur d'écriture est ignorée, comme
|
||||
dans le Go ;
|
||||
- une liste rangée illisible vaut « aucun critère » ;
|
||||
- la note vide n'est pas écrite (`omitempty`).
|
||||
|
||||
`GoTurnHost::exec_tool` l'exécute pour la discussion du tour
|
||||
(`main_conv`). La passe de vérification (`Role` = verifier) est la seule à
|
||||
pouvoir marquer `passed`. Chaque changement pousse la liste à l'écran (delta
|
||||
`criteria` + `ts`) par un nouveau champ `deltas` de l'hôte :
|
||||
|
||||
- le journal de la discussion pour un tour ;
|
||||
- celui de la conversation pour une passe de vérification.
|
||||
|
||||
Écart assumé : le Go rangeait les critères sous la discussion **active**
|
||||
au moment de l'appel (`convEnsureActive`). La façade les range sous celle du
|
||||
tour. Elles ne diffèrent que si l'utilisateur change de discussion pendant
|
||||
la génération, et c'est alors la façade qui a raison.
|
||||
|
||||
`see_image` reste au Go. Il redresse et réduit l'image (EXIF, taille) avant
|
||||
de l'encoder, ce qu'un port ne rendrait pas octet pour octet.
|
||||
|
||||
## Parité
|
||||
|
||||
`internal/loki/parity_criteria_test.go` tire du Go un journal de 22 appels
|
||||
(`tests/parity_tools/criteria.json`). Chaque étape porte :
|
||||
|
||||
- les arguments, builder ou vérification ;
|
||||
- le résultat ;
|
||||
- les critères rangés ensuite.
|
||||
|
||||
`tests/tools.rs` (`criteria_comme_le_go`) le rejoue contre le port. Il
|
||||
vérifie aussi :
|
||||
|
||||
- le rangement scellé ;
|
||||
- la liste poussée à l'écran à chaque changement, et seulement alors.
|
||||
|
||||
`tests/lot6.rs` (`contrat_verifie_par_la_facade`) fait tourner un vrai
|
||||
contrat. La passe de vérification marque le critère `passed` dans la
|
||||
façade : rien ne passe par le Go, le critère est rangé scellé, l'écran
|
||||
reçoit la liste.
|
||||
@@ -9,6 +9,10 @@ package loki
|
||||
//
|
||||
// Rangés par discussion dans bbolt (bucket chat, clé crit:<id>) : ils suivent
|
||||
// la discussion, sa suppression les emporte.
|
||||
//
|
||||
// Port Rust de l'outil (lot 38) : rust/loki-rs/src/chat/crittool.rs, employé
|
||||
// par les tours de la façade ; journal de parité dans
|
||||
// parity_criteria_test.go.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
//go:build linux
|
||||
|
||||
package loki
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// parity_criteria_test.go — journal de référence de l'outil criteria du mode
|
||||
// Code (lot 38 de la migration Rust) : chaque appel (arguments tels que le
|
||||
// modèle les envoie, builder ou vérification), son résultat, et les critères
|
||||
// rangés ensuite pour la discussion. Rangé dans
|
||||
// rust/loki-rs/tests/parity_tools/criteria.json ; tests/tools.rs le rejoue
|
||||
// par le port Rust.
|
||||
//
|
||||
// LOKI_PARITY_UPDATE=1 go test ./internal/loki -run TestPariteCriteres
|
||||
|
||||
const parityCriteriaFile = "../../rust/loki-rs/tests/parity_tools/criteria.json"
|
||||
|
||||
type parityCriteriaStep struct {
|
||||
Name string `json:"name"`
|
||||
Verifier bool `json:"verifier,omitempty"`
|
||||
Put string `json:"put,omitempty"` // critères écrits tels quels avant l'appel
|
||||
Args json.RawMessage `json:"args"`
|
||||
Out string `json:"out"`
|
||||
Stored any `json:"stored"` // la liste rangée (null : aucune)
|
||||
}
|
||||
|
||||
func TestPariteCriteres(t *testing.T) {
|
||||
testHome(t)
|
||||
conv := convEnsureActive()
|
||||
var steps []parityCriteriaStep
|
||||
run := func(st parityCriteriaStep) {
|
||||
if st.Put != "" {
|
||||
if err := putStoreBytes(bkChat, critKey(conv), []byte(st.Put)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
var args map[string]any
|
||||
if err := json.Unmarshal(st.Args, &args); err != nil {
|
||||
t.Fatalf("%s : %v", st.Name, err)
|
||||
}
|
||||
if args == nil {
|
||||
args = map[string]any{}
|
||||
}
|
||||
st.Out = toolCriteria(args, st.Verifier)
|
||||
st.Stored = nil
|
||||
if b, ok := getStoreBytes(bkChat, critKey(conv)); ok && len(b) > 0 {
|
||||
var v any
|
||||
if err := json.Unmarshal(b, &v); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
st.Stored = v
|
||||
}
|
||||
steps = append(steps, st)
|
||||
}
|
||||
a := func(s string) json.RawMessage { return json.RawMessage(s) }
|
||||
|
||||
run(parityCriteriaStep{Name: "liste vide", Args: a(`{"action":"list"}`)})
|
||||
run(parityCriteriaStep{Name: "action absente : liste", Args: a(`{}`)})
|
||||
run(parityCriteriaStep{Name: "action d'un autre type", Args: a(`{"action":3}`)})
|
||||
run(parityCriteriaStep{Name: "ajout sans textes", Args: a(`{"action":"add"}`)})
|
||||
run(parityCriteriaStep{Name: "ajout de textes vides ou d'un autre type", Args: a(`{"action":"add","texts":[" ",4,null]}`)})
|
||||
run(parityCriteriaStep{Name: "ajout", Args: a(`{"action":"add","texts":[" les tests passent ","la doc <à jour> & relue",""]}`)})
|
||||
run(parityCriteriaStep{Name: "ajout à la suite", Args: a(`{"action":"add","texts":["pas de régression"]}`)})
|
||||
run(parityCriteriaStep{Name: "le builder dit fait", Args: a(`{"action":"set","id":1,"status":"completed","note":" vérifié à la main "}`)})
|
||||
run(parityCriteriaStep{Name: "le builder ne valide pas", Args: a(`{"action":"set","id":1,"status":"passed"}`)})
|
||||
run(parityCriteriaStep{Name: "id en chaîne avec #", Verifier: true, Args: a(`{"action":"set","id":" #2 ","status":"failed","note":"lien mort"}`)})
|
||||
run(parityCriteriaStep{Name: "id en chaîne illisible", Verifier: true, Args: a(`{"action":"set","id":"deux","status":"passed"}`)})
|
||||
run(parityCriteriaStep{Name: "id décimal tronqué", Verifier: true, Args: a(`{"action":"set","id":3.9,"status":"passed"}`)})
|
||||
run(parityCriteriaStep{Name: "note seule", Args: a(`{"action":"set","id":"+1","note":"revu"}`)})
|
||||
run(parityCriteriaStep{Name: "statut libre", Args: a(`{"action":"set","id":2,"status":"bloqué"}`)})
|
||||
run(parityCriteriaStep{Name: "id inconnu", Verifier: true, Args: a(`{"action":"set","id":9,"status":"passed"}`)})
|
||||
run(parityCriteriaStep{Name: "liste", Args: a(`{"action":"list"}`)})
|
||||
run(parityCriteriaStep{Name: "action inconnue", Args: a(`{"action":"reset"}`)})
|
||||
run(parityCriteriaStep{Name: "effacer", Args: a(`{"action":"clear"}`)})
|
||||
run(parityCriteriaStep{Name: "effacer à vide", Args: a(`{"action":"clear"}`)})
|
||||
// Vingt au plus ; les ids reprennent après le plus grand.
|
||||
run(parityCriteriaStep{Name: "liste pleine",
|
||||
Put: `[{"id":7,"text":"a","status":"passed"},{"id":3,"text":"b","status":"pending"}]`,
|
||||
Args: a(`{"action":"add","texts":["c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w"]}`)})
|
||||
run(parityCriteriaStep{Name: "liste déjà pleine", Args: a(`{"action":"add","texts":["x"]}`)})
|
||||
run(parityCriteriaStep{Name: "liste rangée illisible", Put: `{"pas":"une liste"}`, Args: a(`{"action":"list"}`)})
|
||||
|
||||
got, err := json.MarshalIndent(steps, "", " ")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got = append(got, '\n')
|
||||
if os.Getenv("LOKI_PARITY_UPDATE") == "1" {
|
||||
if err := os.MkdirAll(filepath.Dir(parityCriteriaFile), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(parityCriteriaFile, got, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
want, err := os.ReadFile(parityCriteriaFile)
|
||||
if err != nil {
|
||||
t.Fatalf("journal de référence absent (%v) : LOKI_PARITY_UPDATE=1 pour le créer", err)
|
||||
}
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("l'outil criteria ne produit plus le journal rangé dans %s.\n"+
|
||||
"LOKI_PARITY_UPDATE=1 pour le régénérer si le changement est voulu.", parityCriteriaFile)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
//! L'outil `criteria` du mode Code (lot 38) — port de `toolCriteria`
|
||||
//! (`code_criteria.go`) : le modèle pose, met à jour, liste ou efface les
|
||||
//! critères d'acceptation de la discussion (`chat/crit:<id>`, scellés comme
|
||||
//! le contenu des discussions). Seule la passe de vérification peut marquer
|
||||
//! un critère `passed`. Chaque changement part aussi à l'écran (delta
|
||||
//! `criteria`, `critNotify`).
|
||||
|
||||
use serde_json::{Map, Value, json};
|
||||
|
||||
use super::persist::BK_CHAT;
|
||||
use super::verify::{Criterion, crit_list, crit_render};
|
||||
use crate::store::Store;
|
||||
|
||||
/// `critMaxCount`.
|
||||
pub const CRIT_MAX_COUNT: usize = 20;
|
||||
|
||||
/// L'issue d'un appel : le texte rendu au modèle, et la liste à pousser à
|
||||
/// l'écran quand elle a changé (vide : effacée).
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub struct CritOut {
|
||||
pub result: String,
|
||||
pub notify: Option<Vec<Criterion>>,
|
||||
}
|
||||
|
||||
impl CritOut {
|
||||
fn text(s: impl Into<String>) -> Self {
|
||||
CritOut {
|
||||
result: s.into(),
|
||||
notify: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `strconv.Atoi` (signe accepté) ; 0 sinon, comme l'id ignoré du Go.
|
||||
fn go_atoi(s: &str) -> i64 {
|
||||
let digits = s.strip_prefix(['+', '-']).unwrap_or(s);
|
||||
if digits.is_empty() || !digits.bytes().all(|c| c.is_ascii_digit()) {
|
||||
return 0;
|
||||
}
|
||||
s.parse().unwrap_or(0)
|
||||
}
|
||||
|
||||
/// `strings.TrimSpace`.
|
||||
fn go_trim(s: &str) -> &str {
|
||||
s.trim_matches(char::is_whitespace)
|
||||
}
|
||||
|
||||
/// `critSave` : une liste vide supprime la clé ; une erreur est ignorée,
|
||||
/// comme dans le Go.
|
||||
async fn crit_save(store: &Store, conv: &str, list: &[Criterion]) {
|
||||
let key = format!("crit:{conv}");
|
||||
if list.is_empty() {
|
||||
let _ = store.del(BK_CHAT, &key).await;
|
||||
return;
|
||||
}
|
||||
let _ = store
|
||||
.put_seal(
|
||||
BK_CHAT,
|
||||
&key,
|
||||
crate::gojson::marshal(&list.to_vec()).into_bytes(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
/// `toolCriteria` : `allow_pass`, la passe de vérification seule.
|
||||
pub async fn exec(
|
||||
store: &Store,
|
||||
conv: &str,
|
||||
args: &Map<String, Value>,
|
||||
allow_pass: bool,
|
||||
) -> CritOut {
|
||||
let s = |k: &str| args.get(k).and_then(Value::as_str).unwrap_or("");
|
||||
let action = s("action");
|
||||
let mut list = crit_list(store, conv).await;
|
||||
match action {
|
||||
"list" | "" => {
|
||||
if list.is_empty() {
|
||||
return CritOut::text("[aucun critère]");
|
||||
}
|
||||
CritOut::text(crit_render(&list))
|
||||
}
|
||||
"add" => {
|
||||
let raw: &[Value] = args
|
||||
.get("texts")
|
||||
.and_then(Value::as_array)
|
||||
.map_or(&[], Vec::as_slice);
|
||||
let mut next = 1;
|
||||
for c in &list {
|
||||
if c.id >= next {
|
||||
next = c.id + 1;
|
||||
}
|
||||
}
|
||||
let mut added = 0;
|
||||
for r in raw {
|
||||
let Some(t) = r.as_str().map(go_trim).filter(|t| !t.is_empty()) else {
|
||||
continue;
|
||||
};
|
||||
if list.len() >= CRIT_MAX_COUNT {
|
||||
break;
|
||||
}
|
||||
list.push(Criterion {
|
||||
id: next,
|
||||
text: t.to_string(),
|
||||
status: "pending".into(),
|
||||
note: String::new(),
|
||||
});
|
||||
next += 1;
|
||||
added += 1;
|
||||
}
|
||||
if added == 0 {
|
||||
return CritOut::text("[erreur] aucun critère fourni (texts)");
|
||||
}
|
||||
crit_save(store, conv, &list).await;
|
||||
CritOut {
|
||||
result: format!("[ok] {added} critère(s) ajouté(s)\n{}", crit_render(&list)),
|
||||
notify: Some(list),
|
||||
}
|
||||
}
|
||||
"set" => {
|
||||
// id numérique ou chaîne (« "2" », « "#2" »).
|
||||
let id = match args.get("id") {
|
||||
Some(Value::Number(n)) => n.as_f64().map_or(0, |v| v as i64),
|
||||
Some(Value::String(v)) => {
|
||||
let t = go_trim(v);
|
||||
go_atoi(t.strip_prefix('#').unwrap_or(t))
|
||||
}
|
||||
_ => 0,
|
||||
};
|
||||
let status = s("status");
|
||||
let note = s("note");
|
||||
if status == "passed" && !allow_pass {
|
||||
return CritOut::text(
|
||||
"[refusé] Seule la passe de vérification peut marquer un critère passed. Termine ton implémentation ; la vérification suivra.",
|
||||
);
|
||||
}
|
||||
let Some(i) = list.iter().position(|c| c.id == id) else {
|
||||
return CritOut::text(format!("[erreur] critère #{id} inconnu"));
|
||||
};
|
||||
if !status.is_empty() {
|
||||
list[i].status = status.to_string();
|
||||
}
|
||||
if !note.is_empty() {
|
||||
list[i].note = go_trim(note).to_string();
|
||||
}
|
||||
crit_save(store, conv, &list).await;
|
||||
let result = format!("[ok] critère #{id} → {}", list[i].status);
|
||||
CritOut {
|
||||
result,
|
||||
notify: Some(list),
|
||||
}
|
||||
}
|
||||
"clear" => {
|
||||
crit_save(store, conv, &[]).await;
|
||||
CritOut {
|
||||
result: "[ok] critères effacés".into(),
|
||||
notify: Some(Vec::new()),
|
||||
}
|
||||
}
|
||||
a => CritOut::text(format!("[erreur] action inconnue : {a}")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Le delta `critNotify` : la liste (vide : `[]`) et l'heure (ms).
|
||||
pub fn notify_delta(list: &[Criterion], ts_ms: i64) -> Map<String, Value> {
|
||||
let mut d = Map::new();
|
||||
d.insert("criteria".into(), json!(list));
|
||||
d.insert("ts".into(), json!(ts_ms));
|
||||
d
|
||||
}
|
||||
|
||||
/// Maintenant, en ms Unix.
|
||||
pub fn now_ms() -> i64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map_or(0, |d| d.as_millis() as i64)
|
||||
}
|
||||
@@ -1012,6 +1012,10 @@ impl ChatService {
|
||||
kwargs: reasoning.kwargs.clone().unwrap_or_default(),
|
||||
},
|
||||
queued: Box::new(move || me.drain_queued(epoch)),
|
||||
deltas: {
|
||||
let me = self.clone();
|
||||
Box::new(move |d| me.push_delta(d))
|
||||
},
|
||||
perf_kind: super::perf::KIND_MAIN,
|
||||
side_slot: Default::default(),
|
||||
projsnap: snap_turn,
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
//! classées et formulées comme `friendlyLLMError` ;
|
||||
//! - outils fichiers et shell exécutés ici ([`crate::tools`], état partagé
|
||||
//! tenu par le Go), outils MCP par le pool de la façade quand elle le tient
|
||||
//! (lot 36), outils task_* quand elle tient les tâches (lot 37) ; les autres
|
||||
//! (lot 36), outils task_* quand elle tient les tâches (lot 37), critères
|
||||
//! du mode Code (lot 38) ; les autres
|
||||
//! par le Go (`/internal/v1/tool/exec`), avec
|
||||
//! ce qu'il tire du résultat (capture de la bulle, aperçu du navigateur,
|
||||
//! image relayée) ;
|
||||
@@ -391,6 +392,9 @@ pub struct GoTurnHost {
|
||||
pub compact: TurnCompact,
|
||||
/// Messages mis en file par l'utilisateur pendant le tour.
|
||||
pub queued: Box<dyn Fn() -> Vec<Message> + Send + Sync>,
|
||||
/// Un événement pour l'écran, hors du flux du tour (la liste des
|
||||
/// critères après l'outil `criteria`).
|
||||
pub deltas: Box<dyn Fn(super::journal::Delta) + Send + Sync>,
|
||||
/// Nature des complétions pour la télémétrie : `main` (tour de la
|
||||
/// discussion), `verify` (passe de vérification du mode Code).
|
||||
pub perf_kind: &'static str,
|
||||
@@ -649,6 +653,24 @@ impl TurnHost for GoTurnHost {
|
||||
// Outils MCP, quand la façade tient le pool : ce que ferait le Go
|
||||
// (`mcpCall` → /internal/facade/v1/mcp/call), sans ses deux
|
||||
// allers-retours. Le Go n'en tire rien d'autre que le texte.
|
||||
// Critères d'acceptation du mode Code (lot 38) : ceux de la
|
||||
// discussion du tour ; seule la vérification marque `passed`.
|
||||
if name == "criteria" {
|
||||
let verifier = self.caps.get("Role").and_then(Value::as_str) == Some("verifier");
|
||||
let out =
|
||||
super::crittool::exec(&self.shared.store, &self.compact.main_conv, args, verifier)
|
||||
.await;
|
||||
if let Some(list) = &out.notify {
|
||||
(self.deltas)(super::crittool::notify_delta(
|
||||
list,
|
||||
super::crittool::now_ms(),
|
||||
));
|
||||
}
|
||||
return ToolOutcome {
|
||||
result: out.result,
|
||||
..Default::default()
|
||||
};
|
||||
}
|
||||
// Outils task_*, quand la façade tient les tâches (lot 37).
|
||||
if crate::tasks::tools::is_tool(name)
|
||||
&& let Some(tasks) = &self.shared.tasks
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
pub mod compact;
|
||||
pub mod conv;
|
||||
pub mod crittool;
|
||||
pub mod engine;
|
||||
pub mod generate;
|
||||
pub mod history;
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Value, json};
|
||||
use tokio::sync::watch;
|
||||
|
||||
@@ -43,7 +43,7 @@ pub const CODE_MAX_FIX_TURNS: usize = 2;
|
||||
pub const CODE_MAX_CONTINUE: usize = 2;
|
||||
|
||||
/// Un critère d'acceptation (`Criterion`).
|
||||
#[derive(Debug, Clone, Default, PartialEq, Deserialize)]
|
||||
#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
|
||||
pub struct Criterion {
|
||||
#[serde(default)]
|
||||
pub id: i64,
|
||||
@@ -52,7 +52,7 @@ pub struct Criterion {
|
||||
/// pending | completed (le builder dit « fait ») | passed | failed.
|
||||
#[serde(default)]
|
||||
pub status: String,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub note: String,
|
||||
}
|
||||
|
||||
@@ -476,6 +476,12 @@ impl ChatService {
|
||||
// Les messages en file attendent la fin du tour (runChatTools sans
|
||||
// injection, comme le Go).
|
||||
queued: Box::new(Vec::new),
|
||||
deltas: {
|
||||
let conv = conv.clone();
|
||||
Box::new(move |d| {
|
||||
conv.append_delta(conv.epoch(), d);
|
||||
})
|
||||
},
|
||||
// Passe de vérification : comptée à part, comme perfVerify.
|
||||
perf_kind: if isolated {
|
||||
super::perf::KIND_VERIFY
|
||||
|
||||
@@ -665,22 +665,6 @@ async fn handle(st: Arc<FakeGo>, req: Request<Incoming>) -> Response<Full<Bytes>
|
||||
"{name} {}",
|
||||
v["caps"]["Role"].as_str().unwrap_or_default()
|
||||
));
|
||||
if name == "criteria" && v["args"]["action"] == "set" {
|
||||
let mut chat = st.store.lock().unwrap();
|
||||
for (k, val) in chat.entry("chat".into()).or_default().iter_mut() {
|
||||
if !k.starts_with("crit:") {
|
||||
continue;
|
||||
}
|
||||
let mut list: Vec<serde_json::Value> =
|
||||
serde_json::from_slice(val).unwrap_or_default();
|
||||
for c in &mut list {
|
||||
if c["id"] == v["args"]["id"] {
|
||||
c["status"] = v["args"]["status"].clone();
|
||||
}
|
||||
}
|
||||
*val = serde_json::to_vec(&list).unwrap();
|
||||
}
|
||||
}
|
||||
let by_name = st.tool_results.lock().unwrap().get(name).cloned();
|
||||
let result = by_name
|
||||
.or_else(|| st.tool_result.lock().unwrap().clone())
|
||||
|
||||
@@ -696,6 +696,7 @@ async fn tour_complet_par_le_go() {
|
||||
kwargs: Default::default(),
|
||||
},
|
||||
queued: Box::new(Vec::new),
|
||||
deltas: Box::new(|_| {}),
|
||||
perf_kind: "main",
|
||||
side_slot: Default::default(),
|
||||
projsnap: None,
|
||||
@@ -1089,6 +1090,7 @@ async fn hote_et_sonde_de_gabarit() {
|
||||
kwargs: Default::default(),
|
||||
},
|
||||
queued: Box::new(Vec::new),
|
||||
deltas: Box::new(|_| {}),
|
||||
perf_kind: "main",
|
||||
side_slot: Default::default(),
|
||||
projsnap: None,
|
||||
@@ -1259,7 +1261,20 @@ async fn contrat_verifie_par_la_facade() {
|
||||
Verify each non-passed criterion now (git_diff first), and record every verdict with the criteria tool."
|
||||
);
|
||||
assert_eq!(v["tools"][0]["function"]["name"], "criteria");
|
||||
assert_eq!(*go.tool_log.lock().unwrap(), ["criteria verifier"]);
|
||||
// L'outil criteria s'exécute dans la façade (lot 38) : rien par le Go ;
|
||||
// le critère rangé (scellé) passe à « passed », et l'écran reçoit la
|
||||
// liste.
|
||||
assert!(go.tool_log.lock().unwrap().is_empty(), "{:?}", go.tool_log);
|
||||
let stored = go.get("chat", &format!("crit:{id}")).unwrap();
|
||||
assert_eq!(
|
||||
stored,
|
||||
r#"SCELLE:[{"id":1,"text":"les tests passent","status":"passed"}]"#
|
||||
);
|
||||
let crit = deltas(&svc, "criteria");
|
||||
assert_eq!(
|
||||
crit,
|
||||
[serde_json::json!([{"id":1,"text":"les tests passent","status":"passed"}])]
|
||||
);
|
||||
assert_eq!(
|
||||
deltas(&svc, "role"),
|
||||
[serde_json::json!("verifier"), serde_json::json!("")]
|
||||
|
||||
@@ -0,0 +1,648 @@
|
||||
[
|
||||
{
|
||||
"name": "liste vide",
|
||||
"args": {
|
||||
"action": "list"
|
||||
},
|
||||
"out": "[aucun critère]",
|
||||
"stored": null
|
||||
},
|
||||
{
|
||||
"name": "action absente : liste",
|
||||
"args": {},
|
||||
"out": "[aucun critère]",
|
||||
"stored": null
|
||||
},
|
||||
{
|
||||
"name": "action d'un autre type",
|
||||
"args": {
|
||||
"action": 3
|
||||
},
|
||||
"out": "[aucun critère]",
|
||||
"stored": null
|
||||
},
|
||||
{
|
||||
"name": "ajout sans textes",
|
||||
"args": {
|
||||
"action": "add"
|
||||
},
|
||||
"out": "[erreur] aucun critère fourni (texts)",
|
||||
"stored": null
|
||||
},
|
||||
{
|
||||
"name": "ajout de textes vides ou d'un autre type",
|
||||
"args": {
|
||||
"action": "add",
|
||||
"texts": [
|
||||
" ",
|
||||
4,
|
||||
null
|
||||
]
|
||||
},
|
||||
"out": "[erreur] aucun critère fourni (texts)",
|
||||
"stored": null
|
||||
},
|
||||
{
|
||||
"name": "ajout",
|
||||
"args": {
|
||||
"action": "add",
|
||||
"texts": [
|
||||
" les tests passent ",
|
||||
"la doc \u003cà jour\u003e \u0026 relue",
|
||||
""
|
||||
]
|
||||
},
|
||||
"out": "[ok] 2 critère(s) ajouté(s)\n[ ] #1 les tests passent\n[ ] #2 la doc \u003cà jour\u003e \u0026 relue",
|
||||
"stored": [
|
||||
{
|
||||
"id": 1,
|
||||
"status": "pending",
|
||||
"text": "les tests passent"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"status": "pending",
|
||||
"text": "la doc \u003cà jour\u003e \u0026 relue"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ajout à la suite",
|
||||
"args": {
|
||||
"action": "add",
|
||||
"texts": [
|
||||
"pas de régression"
|
||||
]
|
||||
},
|
||||
"out": "[ok] 1 critère(s) ajouté(s)\n[ ] #1 les tests passent\n[ ] #2 la doc \u003cà jour\u003e \u0026 relue\n[ ] #3 pas de régression",
|
||||
"stored": [
|
||||
{
|
||||
"id": 1,
|
||||
"status": "pending",
|
||||
"text": "les tests passent"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"status": "pending",
|
||||
"text": "la doc \u003cà jour\u003e \u0026 relue"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"status": "pending",
|
||||
"text": "pas de régression"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "le builder dit fait",
|
||||
"args": {
|
||||
"action": "set",
|
||||
"id": 1,
|
||||
"status": "completed",
|
||||
"note": " vérifié à la main "
|
||||
},
|
||||
"out": "[ok] critère #1 → completed",
|
||||
"stored": [
|
||||
{
|
||||
"id": 1,
|
||||
"note": "vérifié à la main",
|
||||
"status": "completed",
|
||||
"text": "les tests passent"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"status": "pending",
|
||||
"text": "la doc \u003cà jour\u003e \u0026 relue"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"status": "pending",
|
||||
"text": "pas de régression"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "le builder ne valide pas",
|
||||
"args": {
|
||||
"action": "set",
|
||||
"id": 1,
|
||||
"status": "passed"
|
||||
},
|
||||
"out": "[refusé] Seule la passe de vérification peut marquer un critère passed. Termine ton implémentation ; la vérification suivra.",
|
||||
"stored": [
|
||||
{
|
||||
"id": 1,
|
||||
"note": "vérifié à la main",
|
||||
"status": "completed",
|
||||
"text": "les tests passent"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"status": "pending",
|
||||
"text": "la doc \u003cà jour\u003e \u0026 relue"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"status": "pending",
|
||||
"text": "pas de régression"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "id en chaîne avec #",
|
||||
"verifier": true,
|
||||
"args": {
|
||||
"action": "set",
|
||||
"id": " #2 ",
|
||||
"status": "failed",
|
||||
"note": "lien mort"
|
||||
},
|
||||
"out": "[ok] critère #2 → failed",
|
||||
"stored": [
|
||||
{
|
||||
"id": 1,
|
||||
"note": "vérifié à la main",
|
||||
"status": "completed",
|
||||
"text": "les tests passent"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"note": "lien mort",
|
||||
"status": "failed",
|
||||
"text": "la doc \u003cà jour\u003e \u0026 relue"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"status": "pending",
|
||||
"text": "pas de régression"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "id en chaîne illisible",
|
||||
"verifier": true,
|
||||
"args": {
|
||||
"action": "set",
|
||||
"id": "deux",
|
||||
"status": "passed"
|
||||
},
|
||||
"out": "[erreur] critère #0 inconnu",
|
||||
"stored": [
|
||||
{
|
||||
"id": 1,
|
||||
"note": "vérifié à la main",
|
||||
"status": "completed",
|
||||
"text": "les tests passent"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"note": "lien mort",
|
||||
"status": "failed",
|
||||
"text": "la doc \u003cà jour\u003e \u0026 relue"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"status": "pending",
|
||||
"text": "pas de régression"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "id décimal tronqué",
|
||||
"verifier": true,
|
||||
"args": {
|
||||
"action": "set",
|
||||
"id": 3.9,
|
||||
"status": "passed"
|
||||
},
|
||||
"out": "[ok] critère #3 → passed",
|
||||
"stored": [
|
||||
{
|
||||
"id": 1,
|
||||
"note": "vérifié à la main",
|
||||
"status": "completed",
|
||||
"text": "les tests passent"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"note": "lien mort",
|
||||
"status": "failed",
|
||||
"text": "la doc \u003cà jour\u003e \u0026 relue"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"status": "passed",
|
||||
"text": "pas de régression"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "note seule",
|
||||
"args": {
|
||||
"action": "set",
|
||||
"id": "+1",
|
||||
"note": "revu"
|
||||
},
|
||||
"out": "[ok] critère #1 → completed",
|
||||
"stored": [
|
||||
{
|
||||
"id": 1,
|
||||
"note": "revu",
|
||||
"status": "completed",
|
||||
"text": "les tests passent"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"note": "lien mort",
|
||||
"status": "failed",
|
||||
"text": "la doc \u003cà jour\u003e \u0026 relue"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"status": "passed",
|
||||
"text": "pas de régression"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "statut libre",
|
||||
"args": {
|
||||
"action": "set",
|
||||
"id": 2,
|
||||
"status": "bloqué"
|
||||
},
|
||||
"out": "[ok] critère #2 → bloqué",
|
||||
"stored": [
|
||||
{
|
||||
"id": 1,
|
||||
"note": "revu",
|
||||
"status": "completed",
|
||||
"text": "les tests passent"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"note": "lien mort",
|
||||
"status": "bloqué",
|
||||
"text": "la doc \u003cà jour\u003e \u0026 relue"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"status": "passed",
|
||||
"text": "pas de régression"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "id inconnu",
|
||||
"verifier": true,
|
||||
"args": {
|
||||
"action": "set",
|
||||
"id": 9,
|
||||
"status": "passed"
|
||||
},
|
||||
"out": "[erreur] critère #9 inconnu",
|
||||
"stored": [
|
||||
{
|
||||
"id": 1,
|
||||
"note": "revu",
|
||||
"status": "completed",
|
||||
"text": "les tests passent"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"note": "lien mort",
|
||||
"status": "bloqué",
|
||||
"text": "la doc \u003cà jour\u003e \u0026 relue"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"status": "passed",
|
||||
"text": "pas de régression"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "liste",
|
||||
"args": {
|
||||
"action": "list"
|
||||
},
|
||||
"out": "[~] #1 les tests passent — revu\n[ ] #2 la doc \u003cà jour\u003e \u0026 relue — lien mort\n[✓] #3 pas de régression",
|
||||
"stored": [
|
||||
{
|
||||
"id": 1,
|
||||
"note": "revu",
|
||||
"status": "completed",
|
||||
"text": "les tests passent"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"note": "lien mort",
|
||||
"status": "bloqué",
|
||||
"text": "la doc \u003cà jour\u003e \u0026 relue"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"status": "passed",
|
||||
"text": "pas de régression"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "action inconnue",
|
||||
"args": {
|
||||
"action": "reset"
|
||||
},
|
||||
"out": "[erreur] action inconnue : reset",
|
||||
"stored": [
|
||||
{
|
||||
"id": 1,
|
||||
"note": "revu",
|
||||
"status": "completed",
|
||||
"text": "les tests passent"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"note": "lien mort",
|
||||
"status": "bloqué",
|
||||
"text": "la doc \u003cà jour\u003e \u0026 relue"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"status": "passed",
|
||||
"text": "pas de régression"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "effacer",
|
||||
"args": {
|
||||
"action": "clear"
|
||||
},
|
||||
"out": "[ok] critères effacés",
|
||||
"stored": null
|
||||
},
|
||||
{
|
||||
"name": "effacer à vide",
|
||||
"args": {
|
||||
"action": "clear"
|
||||
},
|
||||
"out": "[ok] critères effacés",
|
||||
"stored": null
|
||||
},
|
||||
{
|
||||
"name": "liste pleine",
|
||||
"put": "[{\"id\":7,\"text\":\"a\",\"status\":\"passed\"},{\"id\":3,\"text\":\"b\",\"status\":\"pending\"}]",
|
||||
"args": {
|
||||
"action": "add",
|
||||
"texts": [
|
||||
"c",
|
||||
"d",
|
||||
"e",
|
||||
"f",
|
||||
"g",
|
||||
"h",
|
||||
"i",
|
||||
"j",
|
||||
"k",
|
||||
"l",
|
||||
"m",
|
||||
"n",
|
||||
"o",
|
||||
"p",
|
||||
"q",
|
||||
"r",
|
||||
"s",
|
||||
"t",
|
||||
"u",
|
||||
"v",
|
||||
"w"
|
||||
]
|
||||
},
|
||||
"out": "[ok] 18 critère(s) ajouté(s)\n[✓] #7 a\n[ ] #3 b\n[ ] #8 c\n[ ] #9 d\n[ ] #10 e\n[ ] #11 f\n[ ] #12 g\n[ ] #13 h\n[ ] #14 i\n[ ] #15 j\n[ ] #16 k\n[ ] #17 l\n[ ] #18 m\n[ ] #19 n\n[ ] #20 o\n[ ] #21 p\n[ ] #22 q\n[ ] #23 r\n[ ] #24 s\n[ ] #25 t",
|
||||
"stored": [
|
||||
{
|
||||
"id": 7,
|
||||
"status": "passed",
|
||||
"text": "a"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"status": "pending",
|
||||
"text": "b"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"status": "pending",
|
||||
"text": "c"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"status": "pending",
|
||||
"text": "d"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"status": "pending",
|
||||
"text": "e"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"status": "pending",
|
||||
"text": "f"
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"status": "pending",
|
||||
"text": "g"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"status": "pending",
|
||||
"text": "h"
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"status": "pending",
|
||||
"text": "i"
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"status": "pending",
|
||||
"text": "j"
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"status": "pending",
|
||||
"text": "k"
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"status": "pending",
|
||||
"text": "l"
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"status": "pending",
|
||||
"text": "m"
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"status": "pending",
|
||||
"text": "n"
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"status": "pending",
|
||||
"text": "o"
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"status": "pending",
|
||||
"text": "p"
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"status": "pending",
|
||||
"text": "q"
|
||||
},
|
||||
{
|
||||
"id": 23,
|
||||
"status": "pending",
|
||||
"text": "r"
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"status": "pending",
|
||||
"text": "s"
|
||||
},
|
||||
{
|
||||
"id": 25,
|
||||
"status": "pending",
|
||||
"text": "t"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "liste déjà pleine",
|
||||
"args": {
|
||||
"action": "add",
|
||||
"texts": [
|
||||
"x"
|
||||
]
|
||||
},
|
||||
"out": "[erreur] aucun critère fourni (texts)",
|
||||
"stored": [
|
||||
{
|
||||
"id": 7,
|
||||
"status": "passed",
|
||||
"text": "a"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"status": "pending",
|
||||
"text": "b"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"status": "pending",
|
||||
"text": "c"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"status": "pending",
|
||||
"text": "d"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"status": "pending",
|
||||
"text": "e"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"status": "pending",
|
||||
"text": "f"
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"status": "pending",
|
||||
"text": "g"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"status": "pending",
|
||||
"text": "h"
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"status": "pending",
|
||||
"text": "i"
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"status": "pending",
|
||||
"text": "j"
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"status": "pending",
|
||||
"text": "k"
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"status": "pending",
|
||||
"text": "l"
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"status": "pending",
|
||||
"text": "m"
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"status": "pending",
|
||||
"text": "n"
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"status": "pending",
|
||||
"text": "o"
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"status": "pending",
|
||||
"text": "p"
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"status": "pending",
|
||||
"text": "q"
|
||||
},
|
||||
{
|
||||
"id": 23,
|
||||
"status": "pending",
|
||||
"text": "r"
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"status": "pending",
|
||||
"text": "s"
|
||||
},
|
||||
{
|
||||
"id": 25,
|
||||
"status": "pending",
|
||||
"text": "t"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "liste rangée illisible",
|
||||
"put": "{\"pas\":\"une liste\"}",
|
||||
"args": {
|
||||
"action": "list"
|
||||
},
|
||||
"out": "[aucun critère]",
|
||||
"stored": {
|
||||
"pas": "une liste"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -67,3 +67,53 @@ async fn le_reste_va_au_go() {
|
||||
let args = a(json!({"pattern":"*"}));
|
||||
assert!(exec_local(&st, false, "glob", &args, "*").await.is_some());
|
||||
}
|
||||
|
||||
/// Lot 38 : l'outil criteria du mode Code, rejoué contre le journal du Go
|
||||
/// (`internal/loki/parity_criteria_test.go` → `parity_tools/criteria.json`) :
|
||||
/// résultat de chaque appel, critères rangés après coup (scellés), et la
|
||||
/// liste poussée à l'écran quand elle change.
|
||||
#[tokio::test]
|
||||
async fn criteria_comme_le_go() {
|
||||
use loki_rs::chat::crittool::{CRIT_MAX_COUNT, exec};
|
||||
let fx: Value =
|
||||
serde_json::from_str(include_str!("parity_tools/criteria.json")).expect("journal");
|
||||
let t = tempfile::tempdir().unwrap();
|
||||
let (_f, go, _stop) = common::fakego::start(t.path().into()).await;
|
||||
let backend = go.backend.lock().unwrap().unwrap();
|
||||
let store = loki_rs::store::Store::new(backend, common::fakego::JETON.into());
|
||||
assert_eq!(CRIT_MAX_COUNT, 20);
|
||||
let conv = "c1";
|
||||
for st in fx.as_array().unwrap() {
|
||||
let name = st["name"].as_str().unwrap();
|
||||
if let Some(put) = st["put"].as_str() {
|
||||
store
|
||||
.put_seal("chat", &format!("crit:{conv}"), put.as_bytes().to_vec())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
let args = st["args"].as_object().cloned().unwrap_or_default();
|
||||
let verifier = st["verifier"].as_bool() == Some(true);
|
||||
let before = store.get_open("chat", &format!("crit:{conv}")).await.unwrap();
|
||||
let out = exec(&store, conv, &args, verifier).await;
|
||||
assert_eq!(out.result, st["out"].as_str().unwrap(), "{name}");
|
||||
let raw = store.get_open("chat", &format!("crit:{conv}")).await.unwrap();
|
||||
let stored = raw
|
||||
.as_deref()
|
||||
.filter(|b| !b.is_empty())
|
||||
.map(|b| serde_json::from_slice::<Value>(b).unwrap())
|
||||
.unwrap_or(Value::Null);
|
||||
assert_eq!(stored, st["stored"], "{name}");
|
||||
// Scellés comme le contenu des discussions.
|
||||
if let Some(sealed) = go.get("chat", &format!("crit:{conv}")) {
|
||||
assert!(sealed.starts_with("SCELLE:"), "{name} : {sealed}");
|
||||
}
|
||||
// L'écran reçoit la liste quand elle change (vide : effacée).
|
||||
match &out.notify {
|
||||
Some(list) => {
|
||||
let want = if stored.is_null() { json!([]) } else { stored.clone() };
|
||||
assert_eq!(json!(list), want, "{name}");
|
||||
}
|
||||
None => assert_eq!(raw, before, "{name} : rien ne change sans notification"),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user