Lot 30 : motifs de relance personnalisés tenus par la façade

La surcharge retry_patterns (tableau JSON de regex du Go, chacune en
(?mi)) ne garde plus la conversation au Go : la façade la lit au
démarrage de sa conversation, comme le Go la compile une fois par
processus ; illisible ou vide, les motifs embarqués s'appliquent, une
regex illisible est sautée.

Les regex restent celles du Go : chat::stream::go_regex réécrit pour le
crate regex ce qui n'a pas le même sens — classes Perl et limites de mot
ASCII (\w \d \s \b et leurs contraires, dans et hors d'une classe),
\Q…\E (refusé dans une classe, comme le Go), [ && -- ~~ dans une classe,
octal permis, référence arrière refusée.

Parité : parity_retry_test.go compile côté Go une soixantaine de regex
(dont celles qu'il refuse) et note leur première correspondance sur onze
textes (accents, chiffres non ASCII, espaces insécables, crochets) ;
tests/retry.rs rejoue le journal à l'identique et vérifie la lecture de
la surcharge au démarrage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
This commit is contained in:
Claude committed 2026-10-16 01:24:16 +00:00
1 parent 8a61879bfd
commit e444202b4b
11 files changed
+3633 -11

No files matched your search

+1 -1
View File
@@ -227,7 +227,7 @@ redémarrages (volume `/data`). Variables d'environnement du conteneur :
| `LOKI_AUTO_UPDATE` | `0` : pas de mise à jour des binaires au démarrage | `1` |
| `LOKI_FACADE` | `0` : sans façade Rust, le Go sert directement le port public (porte de sortie) | `1` |
| `LOKI_GO_PORT` | port local (127.0.0.1) du serveur Go derrière la façade | `8091` |
| `LOKI_CHAT` | `rust` : la conversation est tenue par la façade Rust (expérimental, migration en cours), outils fichiers et shell compris, avec l'export, le mode Code et les critères de la discussion. Le Go garde les autres outils et le reste du mode Code. La façade sonde aussi le gabarit du moteur après ses complétions (sonde de gabarit, `NUDGE_IN_TOOL`). Avec `PROJ_SNAPSHOT`, `KEEP_TURN_IMAGES`, `SIDE_SLOT`, `SLOT_PERSIST`, `PREWARM` ou des motifs `retry_patterns`, le Go garde la conversation (décidé au démarrage) | Go |
| `LOKI_CHAT` | `rust` : la conversation est tenue par la façade Rust (expérimental, migration en cours), outils fichiers et shell compris, avec l'export, le mode Code et les critères de la discussion. Le Go garde les autres outils et le reste du mode Code. La façade sonde aussi le gabarit du moteur après ses complétions (sonde de gabarit, `NUDGE_IN_TOOL`), range la télémétrie de ses complétions dans celle du Go et honore les motifs de relance personnalisés (`retry_patterns`). Avec `PROJ_SNAPSHOT`, `KEEP_TURN_IMAGES`, `SIDE_SLOT`, `SLOT_PERSIST` ou `PREWARM`, le Go garde la conversation (décidé au démarrage) | Go |
| `LOKI_MODELS` | `rust` : les routes des modèles — liste, dossiers, suppression, téléchargements, catalogue Hugging Face — sont servies par la façade Rust (expérimental, migration en cours) | Go |
| `LOKI_DICTATE` | `rust` : la dictée vocale (`/api/transcribe`, `/api/dictate/*`) et son serveur de transcription sont tenus par la façade Rust (expérimental, migration en cours) | Go |
| `LOKI_SETTINGS` | `rust` : les réglages partagés — préférences d'affichage, prompt système, intensité du raisonnement, adresse publique — sont servis par la façade Rust (expérimental, migration en cours) | Go |
@@ -0,0 +1,54 @@
# Lot 30 — Motifs de relance personnalisés tenus par la façade
## Contexte
Quand le modèle écrit un appel d'outil en toutes lettres au lieu de
l'émettre par le protocole, Loki le reconnaît et relance le tour avec une
consigne corrective. Les motifs embarqués couvrent les formats connus. Un
expert peut les remplacer par la clé d'état `retry_patterns` :
- c'est un tableau JSON de regex du Go, chacune compilée en `(?mi)` ;
- une regex illisible est sautée ;
- une surcharge illisible ou vide laisse les motifs embarqués.
Jusqu'ici, une surcharge posée gardait la conversation au Go
(`facadeChatBlocker`), même avec `LOKI_CHAT=rust`.
## Le lot
La façade lit la surcharge au démarrage de sa conversation
(`chat::generate::retry_patterns`), comme le Go la compile une fois par
processus. Le Go ne garde plus la conversation pour cette clé.
Les regex sont celles du Go (paquet `regexp`), pas celles de `regex`.
`chat::stream::go_regex` les réécrit pour garder le sens du Go :
- les classes Perl et les limites de mot du Go sont ASCII ; celles de
`regex` sont Unicode :
- hors d'une classe : `\w`, `\W`, `\d`, `\D`, `\s`, `\S` deviennent des
classes ASCII ;
- dans une classe : leur équivalent dans la classe ;
- `\b`, `\B` deviennent `(?-u:\b)`, `(?-u:\B)` ;
- `\Q…\E` (texte littéral) devient le texte échappé ; le Go le refuse dans
une classe, la traduction aussi ;
- dans une classe, `[`, `&&`, `~~` sont littéraux pour le Go, et `--` est un
intervalle : ils sont échappés ;
- l'octal du Go (`\101`) est permis (`octal(true)`). Une référence arrière
(`\1`), que le Go refuse, est refusée.
## Parité
`internal/loki/parity_retry_test.go` compile côté Go une soixantaine de
regex (dont celles qu'il refuse). Il note leur première correspondance sur
onze textes : accents, chiffres non ASCII, espaces insécables, crochets,
majuscules accentuées. Le journal (`tests/parity_retry/patterns.json`) est
rejoué par `tests/retry.rs` : même compilation, même correspondance, même
position.
Autres tests :
- la lecture de la surcharge : absente, `null`, vide, mal typée, suivie de
texte ;
- les regex sautées, les correspondances ;
- la surcharge lue au démarrage de la conversation de la façade ;
- côté Go, la clé ne garde plus la conversation.
+3 -5
View File
@@ -48,9 +48,9 @@ func facadeChat() bool {
// facadeChatBlocker : le réglage qui garde la conversation ici, "" s'il n'y
// en a pas. Ces clés opt-in demandent une machinerie restée au Go (bloc projet
// figé, images gardées, slots du moteur, préchauffage, motifs de relance
// personnalisés). NUDGE_IN_TOOL n'en est plus (lot 23) : la façade a sa sonde
// de gabarit.
// figé, images gardées, slots du moteur, préchauffage). NUDGE_IN_TOOL n'en est
// plus (lot 23) : la façade a sa sonde de gabarit ; ni les motifs de relance
// personnalisés (retry_patterns, lot 30) : la façade les lit et les compile.
func facadeChatBlocker(cfg map[string]string) string {
switch {
case projSnapEnabled(cfg):
@@ -63,8 +63,6 @@ func facadeChatBlocker(cfg map[string]string) string {
return "SLOT_PERSIST"
case prewarmMode(cfg) != "":
return "PREWARM"
case strings.TrimSpace(getStr(bkState, "retry_patterns")) != "":
return "retry_patterns"
}
return ""
}
+6
View File
@@ -308,6 +308,12 @@ func TestFacadeReglageQuiGardeLaConversation(t *testing.T) {
if got := facadeChatBlocker(map[string]string{}); got != "" {
t.Errorf("sans réglage : %q", got)
}
// Les motifs de relance personnalisés ne gardent plus la conversation.
putStr(bkState, "retry_patterns", `["oups"]`)
if got := facadeChatBlocker(map[string]string{}); got != "" {
t.Errorf("retry_patterns : %q", got)
}
putStr(bkState, "retry_patterns", "")
// Sans LOKI_CHAT=rust : le Go, et pourquoi.
oublierDecisionFacade(t)
t.Setenv(facadeChatEnv, "")
+96
View File
@@ -0,0 +1,96 @@
package loki
import (
"os"
"regexp"
"testing"
)
// parity_retry_test.go — journal de référence des motifs retry_patterns
// (lot 30 de la migration Rust) : chaque regex, compilée par le Go en
// (?mi) comme compileRetryPatterns, et sa première correspondance sur des
// textes choisis (classes et limites de mot ASCII, texte littéral, classes
// aux caractères spéciaux, Unicode). Rangé dans
// rust/loki-rs/tests/parity_retry/patterns.json ; tests/retry.rs le rejoue.
//
// LOKI_PARITY_UPDATE=1 go test ./internal/loki -run TestPariteRetryPatterns
const parityRetryFile = "../../rust/loki-rs/tests/parity_retry/patterns.json"
func TestPariteRetryPatterns(t *testing.T) {
pats := []string{
`\w+`, `\W+`, `\d+`, `\D+`, `\s+`, `\S+`, `\bdo_\w+\(`, `\Bfoo`, `\bfoo\b`,
`[\w-]+`, `[^\s]+`, `[\W]+`, `[\d\s]+`, `[\D]+`, `[^\w\s]+`,
`\Qa.b\E`, `\Q(x`, `[\Q]-\E]+`, `a\Q\E+`,
`[a[]+`, `[a&&b]+`, `[!--]+`, `[a--]`, `[~~a]+`, `[]a]+`, `[^]a]+`,
`[[:alpha:]]+`, `[^[:alpha:]]+`, `[[:word:]]+`, `[[:^digit:]]+`,
`^oups`, `\Aoups`, `fin$`, `\pL+`, `\p{Greek}+`, `x{2,3}`, `(?s:a.b)`, `a.b`,
`(?U)a+`, `<function=\w+>`, `\{"name":\s*"(bash|read)"`, `[`, `(`, `\C`, `\z`, `a\z`,
`(?P<n>ab)`, `\x41\x{42}`, `\101`, `\1`, `(a)\1`, `\0`, `\08`, `[\101-\103]+`, `[\x41-\x43]+`, `\.\*\+`, `é+`, `ÉTÉ`,
}
inputs := []string{
"été do_it(1) éfoo foo bar_9",
"x٣42 a  \tb\n",
"axb a.b a(x a]-]b",
"x[a[ x&ab a!#-b x~a x]a [ab",
"1abc ab12 Ωμέγα",
"Bonjour\nOUPS !\nla fin",
"oups au début, puis la FIN",
"aaaa xxx a\nb aéb",
`<function=bash> {"name": "read"}`,
"AB ABC éé ÉTÉ été",
"x.*+y",
}
type match struct {
In int `json:"in"`
Match *string `json:"match"`
At int `json:"at"`
}
type entry struct {
Pattern string `json:"pattern"`
Compiles bool `json:"compiles"`
Matches []match `json:"matches"`
}
journal := map[string]any{"inputs": inputs}
var es []entry
for _, p := range pats {
e := entry{Pattern: p, Matches: []match{}}
re, err := regexp.Compile("(?mi)" + p)
if err == nil {
e.Compiles = true
for i, in := range inputs {
m := match{In: i, At: -1}
if loc := re.FindStringIndex(in); loc != nil {
s := in[loc[0]:loc[1]]
m.Match, m.At = &s, loc[0]
}
e.Matches = append(e.Matches, m)
}
}
es = append(es, e)
}
journal["patterns"] = es
got, err := parityJSON(journal)
if err != nil {
t.Fatal(err)
}
if os.Getenv("LOKI_PARITY_UPDATE") == "1" {
if err := os.MkdirAll("../../rust/loki-rs/tests/parity_retry", 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(parityRetryFile, got, 0o644); err != nil {
t.Fatal(err)
}
return
}
want, err := os.ReadFile(parityRetryFile)
if err != nil {
t.Fatalf("journal de référence absent (%v) : LOKI_PARITY_UPDATE=1 pour le créer", err)
}
if string(got) != string(want) {
t.Errorf("les motifs ne produisent plus le journal rangé dans %s.\n"+
"Si c'est voulu : LOKI_PARITY_UPDATE=1 go test ./internal/loki -run TestPariteRetryPatterns,\n"+
"puis cargo test --test retry dit si le port Rust suit.", parityRetryFile)
}
}
+23 -3
View File
@@ -23,9 +23,10 @@
//! ([`super::verify`]) ; toute compaction rend l'état des critères au résumé.
//!
//! Écarts assumés (restent au Go) : PROJ_SNAPSHOT et KEEP_TURN_IMAGES
//! (refusés par `/turn/view`), préchauffage, motifs `retry_patterns`
//! personnalisés. La télémétrie des complétions (tours, vérifications,
//! résumés) rejoint l'anneau du Go (`/internal/v1/perf/record`).
//! (refusés par `/turn/view`), préchauffage. La télémétrie des complétions
//! (tours, vérifications, résumés) rejoint l'anneau du Go
//! (`/internal/v1/perf/record`) ; les motifs `retry_patterns` personnalisés
//! sont lus au démarrage ([`retry_patterns`]).
use std::collections::VecDeque;
use std::future::Future;
@@ -1053,6 +1054,25 @@ pub async fn go_keeps_chat(store: &crate::store::Store) -> Result<Option<String>
}
}
/// `retryPatterns()` : la surcharge `retry_patterns` de la base si elle se
/// lit, sinon les motifs embarqués. Lue une fois, au démarrage, comme le Go
/// la compile une fois par processus.
pub async fn retry_patterns(
store: &crate::store::Store,
) -> std::sync::Arc<super::stream::RetryPatterns> {
let raw = store.get_str(BK_STATE, "retry_patterns").await;
match super::stream::RetryPatterns::from_override(&raw) {
Some(p) => {
eprintln!(
"[chat] motifs de relance personnalisés (retry_patterns) : {} motif(s)",
p.len()
);
std::sync::Arc::new(p)
}
None => super::stream::RetryPatterns::builtin(),
}
}
impl ChatShared {
/// L'état partagé des tours, autour de l'écrivain des discussions.
/// `img_dir` : `LOKI_HOME/chatimg`.
+215
View File
@@ -909,6 +909,165 @@ impl RetryPatterns {
}
}
impl RetryPatterns {
/// `compileRetryPatterns` : la surcharge `retry_patterns` de la base —
/// un tableau JSON de regex du Go, chacune en `(?mi)`. `None` : absente,
/// illisible ou vide (les motifs embarqués s'appliquent). Une regex qui
/// ne se compile pas est sautée, comme le Go le fait.
pub fn from_override(raw: &str) -> Option<RetryPatterns> {
if raw.is_empty() {
return None;
}
let over: Vec<String> = serde_json::from_str(raw).ok()?;
if over.is_empty() {
return None;
}
let pats = over
.iter()
.filter_map(|p| compile_go_regex(&format!("(?mi){p}")))
.map(|r| Box::new(GoRegex(r)) as Box<dyn RetryPattern>)
.collect();
Some(RetryPatterns::custom(pats))
}
/// Nombre de motifs.
pub fn len(&self) -> usize {
self.pats.len()
}
pub fn is_empty(&self) -> bool {
self.pats.is_empty()
}
}
/// `regexp.Compile` du Go : la regex traduite ([`go_regex`]), octal permis
/// (`\\101`) ; `None` si l'un ou l'autre la refuse.
pub fn compile_go_regex(p: &str) -> Option<regex::Regex> {
regex::RegexBuilder::new(&go_regex(p)?)
.octal(true)
.build()
.ok()
}
/// Un motif de la surcharge, compilé.
struct GoRegex(regex::Regex);
impl RetryPattern for GoRegex {
fn find(&self, haystack: &str) -> Option<(usize, usize)> {
self.0.find(haystack).map(|m| (m.start(), m.end()))
}
}
/// Une regex du Go (RE2, paquet `regexp`) réécrite pour `regex` : les
/// classes Perl et les limites de mot du Go sont ASCII (`\w`, `\d`, `\s`,
/// `\b`), celles de `regex` Unicode ; `\Q…\E` (texte littéral) n'existe
/// pas en Rust ; dans une classe, `[`, `&&`, `--` et `~~` y ont un sens que
/// le Go ne leur donne pas. `None` : une regex que le Go refuse et que
/// `regex` accepterait (`\\Q` dans une classe).
pub fn go_regex(p: &str) -> Option<String> {
let cs: Vec<char> = p.chars().collect();
let mut out = String::with_capacity(p.len() + 16);
let (mut i, mut class, mut class_start) = (0, false, false);
while i < cs.len() {
let c = cs[i];
if c == '\\' && i + 1 < cs.len() {
let n = cs[i + 1];
i += 2;
if n == 'Q' {
// Texte littéral jusqu'à \E, ou jusqu'au bout.
let mut lit = String::new();
while i < cs.len() && !(cs[i] == '\\' && cs.get(i + 1) == Some(&'E')) {
lit.push(cs[i]);
i += 1;
}
if i < cs.len() {
i += 2;
}
if class {
return None;
}
out.push_str(&regex::escape(&lit));
class_start = false;
continue;
}
// Un chiffre seul est une référence arrière pour le Go, qui la
// refuse ; avec l'octal permis, `regex` l'accepterait.
if ('1'..='7').contains(&n) && !cs.get(i).is_some_and(|d| ('0'..='7').contains(d)) {
return None;
}
let rep: &str = match (class, n) {
(false, 'w') => "[[:word:]]",
(false, 'W') => "[[:^word:]]",
(false, 'd') => "[0-9]",
(false, 'D') => "[^0-9]",
(false, 's') => r"[\t\n\f\r ]",
(false, 'S') => r"[^\t\n\f\r ]",
(false, 'b') => r"(?-u:\b)",
(false, 'B') => r"(?-u:\B)",
(true, 'w') => "[:word:]",
(true, 'W') => "[:^word:]",
(true, 'd') => "0-9",
(true, 'D') => "[:^digit:]",
(true, 's') => r"\t\n\f\r ",
(true, 'S') => "[:^space:]",
_ => {
out.push('\\');
out.push(n);
class_start = false;
continue;
}
};
out.push_str(rep);
class_start = false;
continue;
}
if class {
match c {
']' if !class_start => {
class = false;
out.push(']');
}
'[' if cs.get(i + 1) == Some(&':') => {
// Classe POSIX : recopiée jusqu'à « :] ».
let rest: String = cs[i..].iter().collect();
match rest.find(":]") {
Some(end) => {
out.push_str(&rest[..end + 2]);
i += rest[..end + 2].chars().count();
class_start = false;
continue;
}
None => out.push_str(r"\["),
}
}
'[' | '&' | '~' => {
out.push('\\');
out.push(c);
}
'-' if cs.get(i + 1) == Some(&'-') => {
out.push_str(r"-\-");
i += 1;
}
_ => out.push(c),
}
class_start = false;
i += 1;
continue;
}
out.push(c);
i += 1;
if c == '[' {
class = true;
class_start = true;
if cs.get(i) == Some(&'^') {
out.push('^');
i += 1;
}
}
}
Some(out)
}
/// Marge relue avant la fin du balayage précédent.
const RETRY_SCAN_MARGIN: usize = 512;
/// Recul au-delà duquel on retombe sur le balayage complet.
@@ -2673,6 +2832,62 @@ mod tests {
assert_eq!(retry_scan_start(&"a b ".repeat(20000), 80000), 0);
}
/// La surcharge `retry_patterns` : tableau JSON de regex du Go, chacune
/// en `(?mi)`, sémantique du Go (classes et limites de mot ASCII).
#[test]
fn surcharge_retry_patterns() {
for raw in ["", "null", "[]", "[\"a\", 1]", "\"x\"", "[\"a\"] x"] {
assert!(RetryPatterns::from_override(raw).is_none(), "{raw}");
}
let p = RetryPatterns::from_override(r#"["^oups", "[", "\\bdo_\\w+\\("]"#).unwrap();
assert!(p.is_custom());
assert_eq!(p.len(), 2, "la regex illisible est sautée");
assert!(p.textual_tool_call("Bonjour\nOUPS !"));
assert!(p.textual_tool_call("éxécute do_it(1)"));
assert_eq!(
p.snippet("puis do_it(1) et la suite"),
"do_it(1) et la suite"
);
assert!(!p.textual_tool_call("ado_it(1)"));
// Classes du Go : ASCII.
let m = |pat: &str, s: &str| {
compile_go_regex(&format!("(?mi){pat}"))
.unwrap()
.find(s)
.map(|m| m.as_str().to_string())
};
assert_eq!(m(r"\w+", "été"), Some("t".into()));
assert_eq!(m(r"\bfoo\b", "éfoo"), Some("foo".into()));
assert_eq!(m(r"\Bfoo", "éfoo"), None);
assert_eq!(m(r"\d+", "x٣42"), Some("42".into()));
assert_eq!(m(r"\s+", "a\u{a0} \tb"), Some(" \t".into()));
assert_eq!(m(r"\S+", "\u{a0}a b"), Some("\u{a0}a".into()));
assert_eq!(m(r"[\w-]+", "é-a_b c"), Some("-a_b".into()));
assert_eq!(m(r"[^\s]+", " ab "), Some("ab".into()));
assert_eq!(m(r"[\W]", "aé"), Some("é".into()));
assert_eq!(m(r"[\d\s]+", "a1 2b"), Some("1 2".into()));
assert_eq!(m(r"[\D]", "1x"), Some("x".into()));
// Texte littéral, dans et hors d'une classe.
assert_eq!(m(r"\Qa.b\E", "axb a.b"), Some("a.b".into()));
assert_eq!(m(r"\Q(x", "a(x"), Some("(x".into()));
// Le Go refuse \Q dans une classe.
assert_eq!(go_regex(r"[\Q]-\E]+"), None);
// Dans une classe, « [ », « && », « -- » et « ~~ » sont littéraux.
assert_eq!(m(r"[a[]+", "x[a["), Some("[a[".into()));
assert_eq!(m(r"[a&&b]+", "x&ab"), Some("&ab".into()));
// « !-- » : l'intervalle de « ! » à « - » pour le Go.
assert_eq!(m(r"[!--]+", "a!#-b"), Some("!#-".into()));
assert_eq!(m(r"[~~a]+", "x~a"), Some("~a".into()));
assert_eq!(m(r"[]a]+", "x]a"), Some("]a".into()));
assert_eq!(m(r"[[:alpha:]]+", "1abc"), Some("abc".into()));
assert_eq!(m(r"[^[:alpha:]]+", "ab12"), Some("12".into()));
// Le reste passe tel quel.
assert_eq!(
go_regex(r"\pL\.x{2}(?s:.)").as_deref(),
Some(r"\pL\.x{2}(?s:.)")
);
}
/// `(?mi)\Aoups` : motif de surcharge ancré en tête.
struct Oups;
+2 -1
View File
@@ -510,7 +510,8 @@ async fn start_chat(
sessions::GoSessionHooks(store.clone()),
);
s.load().await?;
let shared = ChatShared::for_sessions(&s, store, home.join("chatimg"));
let mut shared = ChatShared::for_sessions(&s, store.clone(), home.join("chatimg"));
shared.patterns = loki_rs::chat::generate::retry_patterns(&store).await;
eprintln!("[loki-rs] conversation tenue par la façade (LOKI_CHAT=rust)");
Ok(ChatService::new(
std::sync::Arc::new(s),
+2 -1
View File
@@ -869,7 +869,8 @@ pub async fn start_with(
sessions::GoSessionHooks(store.clone()),
);
s.load().await.unwrap();
let shared = ChatShared::for_sessions(&s, store, home.join("chatimg"));
let mut shared = ChatShared::for_sessions(&s, store.clone(), home.join("chatimg"));
shared.patterns = loki_rs::chat::generate::retry_patterns(&store).await;
Some(ChatService::new(Arc::new(s), Arc::new(shared)))
} else {
None
File diff suppressed because it is too large. Load diff
+68
View File
@@ -0,0 +1,68 @@
//! Lot 30 : les motifs `retry_patterns` d'une surcharge, rejoués sur le
//! journal tiré du Go (`internal/loki/parity_retry_test.go` →
//! `tests/parity_retry/patterns.json`) : même compilation, même première
//! correspondance.
mod common;
use common::fakego;
use loki_rs::chat::stream::compile_go_regex;
use serde_json::Value;
#[test]
fn motifs_comme_le_go() {
let fx: Value =
serde_json::from_str(include_str!("parity_retry/patterns.json")).expect("journal");
let inputs: Vec<&str> = fx["inputs"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap())
.collect();
for e in fx["patterns"].as_array().unwrap() {
let pat = e["pattern"].as_str().unwrap();
let re = compile_go_regex(&format!("(?mi){pat}"));
assert_eq!(re.is_some(), e["compiles"].as_bool().unwrap(), "{pat}");
let Some(re) = re else { continue };
for m in e["matches"].as_array().unwrap() {
let input = inputs[m["in"].as_u64().unwrap() as usize];
let got = re
.find(input)
.map(|x| (x.as_str().to_string(), x.start() as i64));
let want = m["match"]
.as_str()
.map(|s| (s.to_string(), m["at"].as_i64().unwrap()));
assert_eq!(got, want, "{pat} sur {input:?}");
}
}
}
/// La surcharge est lue au démarrage de la conversation de la façade ;
/// absente ou illisible, les motifs embarqués s'appliquent.
#[tokio::test]
async fn surcharge_lue_au_demarrage() {
for (seed, custom, n) in [
(Some(r#"["^oups", "[", "\\bdo_\\w+\\("]"#), true, 2),
(Some("pas du json"), false, 0),
(None, false, 0),
] {
let tmp = tempfile::tempdir().unwrap();
let (_f, _go, _stop, chat) = fakego::start_with(
tmp.path().to_path_buf(),
fakego::Opts {
chat: true,
seed: seed
.map(|s| vec![("state".into(), "retry_patterns".into(), s.into())])
.unwrap_or_default(),
..Default::default()
},
)
.await;
let p = &chat.unwrap().shared.patterns;
assert_eq!(p.is_custom(), custom, "{seed:?}");
if custom {
assert_eq!(p.len(), n);
assert!(p.textual_tool_call("OUPS, raté"));
}
}
}