From e444202b4b2dcec5aca3583811b0e5f0c96d6d79 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 01:24:16 +0000 Subject: [PATCH] =?UTF-8?q?Lot=2030=20:=20motifs=20de=20relance=20personna?= =?UTF-8?q?lis=C3=A9s=20tenus=20par=20la=20fa=C3=A7ade?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv --- README.md | 2 +- ...igration-lot30-motifs-de-relance-design.md | 54 + internal/loki/facade_chat.go | 8 +- internal/loki/facade_chat_test.go | 6 + internal/loki/parity_retry_test.go | 96 + rust/loki-rs/src/chat/generate.rs | 26 +- rust/loki-rs/src/chat/stream.rs | 215 ++ rust/loki-rs/src/main.rs | 3 +- rust/loki-rs/tests/common/fakego.rs | 3 +- rust/loki-rs/tests/parity_retry/patterns.json | 3163 +++++++++++++++++ rust/loki-rs/tests/retry.rs | 68 + 11 files changed, 3633 insertions(+), 11 deletions(-) create mode 100644 docs/superpowers/specs/2026-10-16-rust-migration-lot30-motifs-de-relance-design.md create mode 100644 internal/loki/parity_retry_test.go create mode 100644 rust/loki-rs/tests/parity_retry/patterns.json create mode 100644 rust/loki-rs/tests/retry.rs diff --git a/README.md b/README.md index 2892ed8..9ee4f90 100644 --- a/README.md +++ b/README.md @@ -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 | diff --git a/docs/superpowers/specs/2026-10-16-rust-migration-lot30-motifs-de-relance-design.md b/docs/superpowers/specs/2026-10-16-rust-migration-lot30-motifs-de-relance-design.md new file mode 100644 index 0000000..b7c942c --- /dev/null +++ b/docs/superpowers/specs/2026-10-16-rust-migration-lot30-motifs-de-relance-design.md @@ -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. diff --git a/internal/loki/facade_chat.go b/internal/loki/facade_chat.go index 46852f2..69b312c 100644 --- a/internal/loki/facade_chat.go +++ b/internal/loki/facade_chat.go @@ -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 "" } diff --git a/internal/loki/facade_chat_test.go b/internal/loki/facade_chat_test.go index e0f0bac..d806341 100644 --- a/internal/loki/facade_chat_test.go +++ b/internal/loki/facade_chat_test.go @@ -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, "") diff --git a/internal/loki/parity_retry_test.go b/internal/loki/parity_retry_test.go new file mode 100644 index 0000000..0455d6e --- /dev/null +++ b/internal/loki/parity_retry_test.go @@ -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+`, ``, `\{"name":\s*"(bash|read)"`, `[`, `(`, `\C`, `\z`, `a\z`, + `(?Pab)`, `\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", + ` {"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) + } +} diff --git a/rust/loki-rs/src/chat/generate.rs b/rust/loki-rs/src/chat/generate.rs index cfa10e8..e82a78a 100644 --- a/rust/loki-rs/src/chat/generate.rs +++ b/rust/loki-rs/src/chat/generate.rs @@ -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 } } +/// `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 { + 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`. diff --git a/rust/loki-rs/src/chat/stream.rs b/rust/loki-rs/src/chat/stream.rs index 3d1bd80..1b0d83f 100644 --- a/rust/loki-rs/src/chat/stream.rs +++ b/rust/loki-rs/src/chat/stream.rs @@ -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 { + if raw.is_empty() { + return None; + } + let over: Vec = 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) + .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::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 { + let cs: Vec = 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(®ex::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; diff --git a/rust/loki-rs/src/main.rs b/rust/loki-rs/src/main.rs index 4fc942a..5c058ff 100644 --- a/rust/loki-rs/src/main.rs +++ b/rust/loki-rs/src/main.rs @@ -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), diff --git a/rust/loki-rs/tests/common/fakego.rs b/rust/loki-rs/tests/common/fakego.rs index d8304e8..fe81056 100644 --- a/rust/loki-rs/tests/common/fakego.rs +++ b/rust/loki-rs/tests/common/fakego.rs @@ -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 diff --git a/rust/loki-rs/tests/parity_retry/patterns.json b/rust/loki-rs/tests/parity_retry/patterns.json new file mode 100644 index 0000000..14919db --- /dev/null +++ b/rust/loki-rs/tests/parity_retry/patterns.json @@ -0,0 +1,3163 @@ +{ + "inputs": [ + "é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", + " {\"name\": \"read\"}", + "AB ABC éé ÉTÉ été", + "x.*+y" + ], + "patterns": [ + { + "pattern": "\\w+", + "compiles": true, + "matches": [ + { + "in": 0, + "match": "t", + "at": 2 + }, + { + "in": 1, + "match": "x", + "at": 0 + }, + { + "in": 2, + "match": "axb", + "at": 0 + }, + { + "in": 3, + "match": "x", + "at": 0 + }, + { + "in": 4, + "match": "1abc", + "at": 0 + }, + { + "in": 5, + "match": "Bonjour", + "at": 0 + }, + { + "in": 6, + "match": "oups", + "at": 0 + }, + { + "in": 7, + "match": "aaaa", + "at": 0 + }, + { + "in": 8, + "match": "function", + "at": 1 + }, + { + "in": 9, + "match": "AB", + "at": 0 + }, + { + "in": 10, + "match": "x", + "at": 0 + } + ] + }, + { + "pattern": "\\W+", + "compiles": true, + "matches": [ + { + "in": 0, + "match": "é", + "at": 0 + }, + { + "in": 1, + "match": "٣", + "at": 1 + }, + { + "in": 2, + "match": " ", + "at": 3 + }, + { + "in": 3, + "match": "[", + "at": 1 + }, + { + "in": 4, + "match": " ", + "at": 4 + }, + { + "in": 5, + "match": "\n", + "at": 7 + }, + { + "in": 6, + "match": " ", + "at": 4 + }, + { + "in": 7, + "match": " ", + "at": 4 + }, + { + "in": 8, + "match": "<", + "at": 0 + }, + { + "in": 9, + "match": " ", + "at": 2 + }, + { + "in": 10, + "match": ".*+", + "at": 1 + } + ] + }, + { + "pattern": "\\d+", + "compiles": true, + "matches": [ + { + "in": 0, + "match": "1", + "at": 12 + }, + { + "in": 1, + "match": "42", + "at": 3 + }, + { + "in": 2, + "match": null, + "at": -1 + }, + { + "in": 3, + "match": null, + "at": -1 + }, + { + "in": 4, + "match": "1", + "at": 0 + }, + { + "in": 5, + "match": null, + "at": -1 + }, + { + "in": 6, + "match": null, + "at": -1 + }, + { + "in": 7, + "match": null, + "at": -1 + }, + { + "in": 8, + "match": null, + "at": -1 + }, + { + "in": 9, + "match": null, + "at": -1 + }, + { + "in": 10, + "match": null, + "at": -1 + } + ] + }, + { + "pattern": "\\D+", + "compiles": true, + "matches": [ + { + "in": 0, + "match": "été do_it(", + "at": 0 + }, + { + "in": 1, + "match": "x٣", + "at": 0 + }, + { + "in": 2, + "match": "axb a.b a(x a]-]b", + "at": 0 + }, + { + "in": 3, + "match": "x[a[ x&ab a!#-b x~a x]a [ab", + "at": 0 + }, + { + "in": 4, + "match": "abc ab", + "at": 1 + }, + { + "in": 5, + "match": "Bonjour\nOUPS !\nla fin", + "at": 0 + }, + { + "in": 6, + "match": "oups au début, puis la FIN", + "at": 0 + }, + { + "in": 7, + "match": "aaaa xxx a\nb aéb", + "at": 0 + }, + { + "in": 8, + "match": " {\"name\": \"read\"}", + "at": 0 + }, + { + "in": 9, + "match": "AB ABC éé ÉTÉ été", + "at": 0 + }, + { + "in": 10, + "match": "x.*+y", + "at": 0 + } + ] + }, + { + "pattern": "\\s+", + "compiles": true, + "matches": [ + { + "in": 0, + "match": " ", + "at": 5 + }, + { + "in": 1, + "match": " ", + "at": 5 + }, + { + "in": 2, + "match": " ", + "at": 3 + }, + { + "in": 3, + "match": " ", + "at": 4 + }, + { + "in": 4, + "match": " ", + "at": 4 + }, + { + "in": 5, + "match": "\n", + "at": 7 + }, + { + "in": 6, + "match": " ", + "at": 4 + }, + { + "in": 7, + "match": " ", + "at": 4 + }, + { + "in": 8, + "match": " ", + "at": 15 + }, + { + "in": 9, + "match": " ", + "at": 2 + }, + { + "in": 10, + "match": null, + "at": -1 + } + ] + }, + { + "pattern": "\\S+", + "compiles": true, + "matches": [ + { + "in": 0, + "match": "été", + "at": 0 + }, + { + "in": 1, + "match": "x٣42", + "at": 0 + }, + { + "in": 2, + "match": "axb", + "at": 0 + }, + { + "in": 3, + "match": "x[a[", + "at": 0 + }, + { + "in": 4, + "match": "1abc", + "at": 0 + }, + { + "in": 5, + "match": "Bonjour", + "at": 0 + }, + { + "in": 6, + "match": "oups", + "at": 0 + }, + { + "in": 7, + "match": "aaaa", + "at": 0 + }, + { + "in": 8, + "match": "", + "at": 0 + }, + { + "in": 9, + "match": "AB", + "at": 0 + }, + { + "in": 10, + "match": "x.*+y", + "at": 0 + } + ] + }, + { + "pattern": "\\bdo_\\w+\\(", + "compiles": true, + "matches": [ + { + "in": 0, + "match": "do_it(", + "at": 6 + }, + { + "in": 1, + "match": null, + "at": -1 + }, + { + "in": 2, + "match": null, + "at": -1 + }, + { + "in": 3, + "match": null, + "at": -1 + }, + { + "in": 4, + "match": null, + "at": -1 + }, + { + "in": 5, + "match": null, + "at": -1 + }, + { + "in": 6, + "match": null, + "at": -1 + }, + { + "in": 7, + "match": null, + "at": -1 + }, + { + "in": 8, + "match": null, + "at": -1 + }, + { + "in": 9, + "match": null, + "at": -1 + }, + { + "in": 10, + "match": null, + "at": -1 + } + ] + }, + { + "pattern": "\\Bfoo", + "compiles": true, + "matches": [ + { + "in": 0, + "match": null, + "at": -1 + }, + { + "in": 1, + "match": null, + "at": -1 + }, + { + "in": 2, + "match": null, + "at": -1 + }, + { + "in": 3, + "match": null, + "at": -1 + }, + { + "in": 4, + "match": null, + "at": -1 + }, + { + "in": 5, + "match": null, + "at": -1 + }, + { + "in": 6, + "match": null, + "at": -1 + }, + { + "in": 7, + "match": null, + "at": -1 + }, + { + "in": 8, + "match": null, + "at": -1 + }, + { + "in": 9, + "match": null, + "at": -1 + }, + { + "in": 10, + "match": null, + "at": -1 + } + ] + }, + { + "pattern": "\\bfoo\\b", + "compiles": true, + "matches": [ + { + "in": 0, + "match": "foo", + "at": 17 + }, + { + "in": 1, + "match": null, + "at": -1 + }, + { + "in": 2, + "match": null, + "at": -1 + }, + { + "in": 3, + "match": null, + "at": -1 + }, + { + "in": 4, + "match": null, + "at": -1 + }, + { + "in": 5, + "match": null, + "at": -1 + }, + { + "in": 6, + "match": null, + "at": -1 + }, + { + "in": 7, + "match": null, + "at": -1 + }, + { + "in": 8, + "match": null, + "at": -1 + }, + { + "in": 9, + "match": null, + "at": -1 + }, + { + "in": 10, + "match": null, + "at": -1 + } + ] + }, + { + "pattern": "[\\w-]+", + "compiles": true, + "matches": [ + { + "in": 0, + "match": "t", + "at": 2 + }, + { + "in": 1, + "match": "x", + "at": 0 + }, + { + "in": 2, + "match": "axb", + "at": 0 + }, + { + "in": 3, + "match": "x", + "at": 0 + }, + { + "in": 4, + "match": "1abc", + "at": 0 + }, + { + "in": 5, + "match": "Bonjour", + "at": 0 + }, + { + "in": 6, + "match": "oups", + "at": 0 + }, + { + "in": 7, + "match": "aaaa", + "at": 0 + }, + { + "in": 8, + "match": "function", + "at": 1 + }, + { + "in": 9, + "match": "AB", + "at": 0 + }, + { + "in": 10, + "match": "x", + "at": 0 + } + ] + }, + { + "pattern": "[^\\s]+", + "compiles": true, + "matches": [ + { + "in": 0, + "match": "été", + "at": 0 + }, + { + "in": 1, + "match": "x٣42", + "at": 0 + }, + { + "in": 2, + "match": "axb", + "at": 0 + }, + { + "in": 3, + "match": "x[a[", + "at": 0 + }, + { + "in": 4, + "match": "1abc", + "at": 0 + }, + { + "in": 5, + "match": "Bonjour", + "at": 0 + }, + { + "in": 6, + "match": "oups", + "at": 0 + }, + { + "in": 7, + "match": "aaaa", + "at": 0 + }, + { + "in": 8, + "match": "", + "at": 0 + }, + { + "in": 9, + "match": "AB", + "at": 0 + }, + { + "in": 10, + "match": "x.*+y", + "at": 0 + } + ] + }, + { + "pattern": "[\\W]+", + "compiles": true, + "matches": [ + { + "in": 0, + "match": "é", + "at": 0 + }, + { + "in": 1, + "match": "٣", + "at": 1 + }, + { + "in": 2, + "match": " ", + "at": 3 + }, + { + "in": 3, + "match": "[", + "at": 1 + }, + { + "in": 4, + "match": " ", + "at": 4 + }, + { + "in": 5, + "match": "\n", + "at": 7 + }, + { + "in": 6, + "match": " ", + "at": 4 + }, + { + "in": 7, + "match": " ", + "at": 4 + }, + { + "in": 8, + "match": "<", + "at": 0 + }, + { + "in": 9, + "match": " ", + "at": 2 + }, + { + "in": 10, + "match": ".*+", + "at": 1 + } + ] + }, + { + "pattern": "[\\d\\s]+", + "compiles": true, + "matches": [ + { + "in": 0, + "match": " ", + "at": 5 + }, + { + "in": 1, + "match": "42 ", + "at": 3 + }, + { + "in": 2, + "match": " ", + "at": 3 + }, + { + "in": 3, + "match": " ", + "at": 4 + }, + { + "in": 4, + "match": "1", + "at": 0 + }, + { + "in": 5, + "match": "\n", + "at": 7 + }, + { + "in": 6, + "match": " ", + "at": 4 + }, + { + "in": 7, + "match": " ", + "at": 4 + }, + { + "in": 8, + "match": " ", + "at": 15 + }, + { + "in": 9, + "match": " ", + "at": 2 + }, + { + "in": 10, + "match": null, + "at": -1 + } + ] + }, + { + "pattern": "[\\D]+", + "compiles": true, + "matches": [ + { + "in": 0, + "match": "été do_it(", + "at": 0 + }, + { + "in": 1, + "match": "x٣", + "at": 0 + }, + { + "in": 2, + "match": "axb a.b a(x a]-]b", + "at": 0 + }, + { + "in": 3, + "match": "x[a[ x&ab a!#-b x~a x]a [ab", + "at": 0 + }, + { + "in": 4, + "match": "abc ab", + "at": 1 + }, + { + "in": 5, + "match": "Bonjour\nOUPS !\nla fin", + "at": 0 + }, + { + "in": 6, + "match": "oups au début, puis la FIN", + "at": 0 + }, + { + "in": 7, + "match": "aaaa xxx a\nb aéb", + "at": 0 + }, + { + "in": 8, + "match": " {\"name\": \"read\"}", + "at": 0 + }, + { + "in": 9, + "match": "AB ABC éé ÉTÉ été", + "at": 0 + }, + { + "in": 10, + "match": "x.*+y", + "at": 0 + } + ] + }, + { + "pattern": "[^\\w\\s]+", + "compiles": true, + "matches": [ + { + "in": 0, + "match": "é", + "at": 0 + }, + { + "in": 1, + "match": "٣", + "at": 1 + }, + { + "in": 2, + "match": ".", + "at": 5 + }, + { + "in": 3, + "match": "[", + "at": 1 + }, + { + "in": 4, + "match": "Ωμέγα", + "at": 10 + }, + { + "in": 5, + "match": "!", + "at": 13 + }, + { + "in": 6, + "match": "é", + "at": 9 + }, + { + "in": 7, + "match": "é", + "at": 14 + }, + { + "in": 8, + "match": "<", + "at": 0 + }, + { + "in": 9, + "match": "éé", + "at": 7 + }, + { + "in": 10, + "match": ".*+", + "at": 1 + } + ] + }, + { + "pattern": "\\Qa.b\\E", + "compiles": true, + "matches": [ + { + "in": 0, + "match": null, + "at": -1 + }, + { + "in": 1, + "match": null, + "at": -1 + }, + { + "in": 2, + "match": "a.b", + "at": 4 + }, + { + "in": 3, + "match": null, + "at": -1 + }, + { + "in": 4, + "match": null, + "at": -1 + }, + { + "in": 5, + "match": null, + "at": -1 + }, + { + "in": 6, + "match": null, + "at": -1 + }, + { + "in": 7, + "match": null, + "at": -1 + }, + { + "in": 8, + "match": null, + "at": -1 + }, + { + "in": 9, + "match": null, + "at": -1 + }, + { + "in": 10, + "match": null, + "at": -1 + } + ] + }, + { + "pattern": "\\Q(x", + "compiles": true, + "matches": [ + { + "in": 0, + "match": null, + "at": -1 + }, + { + "in": 1, + "match": null, + "at": -1 + }, + { + "in": 2, + "match": "(x", + "at": 9 + }, + { + "in": 3, + "match": null, + "at": -1 + }, + { + "in": 4, + "match": null, + "at": -1 + }, + { + "in": 5, + "match": null, + "at": -1 + }, + { + "in": 6, + "match": null, + "at": -1 + }, + { + "in": 7, + "match": null, + "at": -1 + }, + { + "in": 8, + "match": null, + "at": -1 + }, + { + "in": 9, + "match": null, + "at": -1 + }, + { + "in": 10, + "match": null, + "at": -1 + } + ] + }, + { + "pattern": "[\\Q]-\\E]+", + "compiles": false, + "matches": [] + }, + { + "pattern": "a\\Q\\E+", + "compiles": true, + "matches": [ + { + "in": 0, + "match": "a", + "at": 26 + }, + { + "in": 1, + "match": "a", + "at": 6 + }, + { + "in": 2, + "match": "a", + "at": 0 + }, + { + "in": 3, + "match": "a", + "at": 2 + }, + { + "in": 4, + "match": "a", + "at": 1 + }, + { + "in": 5, + "match": "a", + "at": 16 + }, + { + "in": 6, + "match": "a", + "at": 5 + }, + { + "in": 7, + "match": "aaaa", + "at": 0 + }, + { + "in": 8, + "match": "a", + "at": 11 + }, + { + "in": 9, + "match": "A", + "at": 0 + }, + { + "in": 10, + "match": null, + "at": -1 + } + ] + }, + { + "pattern": "[a[]+", + "compiles": true, + "matches": [ + { + "in": 0, + "match": "a", + "at": 26 + }, + { + "in": 1, + "match": "a", + "at": 6 + }, + { + "in": 2, + "match": "a", + "at": 0 + }, + { + "in": 3, + "match": "[a[", + "at": 1 + }, + { + "in": 4, + "match": "a", + "at": 1 + }, + { + "in": 5, + "match": "a", + "at": 16 + }, + { + "in": 6, + "match": "a", + "at": 5 + }, + { + "in": 7, + "match": "aaaa", + "at": 0 + }, + { + "in": 8, + "match": "a", + "at": 11 + }, + { + "in": 9, + "match": "A", + "at": 0 + }, + { + "in": 10, + "match": null, + "at": -1 + } + ] + }, + { + "pattern": "[a&&b]+", + "compiles": true, + "matches": [ + { + "in": 0, + "match": "ba", + "at": 25 + }, + { + "in": 1, + "match": "a", + "at": 6 + }, + { + "in": 2, + "match": "a", + "at": 0 + }, + { + "in": 3, + "match": "a", + "at": 2 + }, + { + "in": 4, + "match": "ab", + "at": 1 + }, + { + "in": 5, + "match": "B", + "at": 0 + }, + { + "in": 6, + "match": "a", + "at": 5 + }, + { + "in": 7, + "match": "aaaa", + "at": 0 + }, + { + "in": 8, + "match": "ba", + "at": 10 + }, + { + "in": 9, + "match": "AB", + "at": 0 + }, + { + "in": 10, + "match": null, + "at": -1 + } + ] + }, + { + "pattern": "[!--]+", + "compiles": true, + "matches": [ + { + "in": 0, + "match": "(", + "at": 11 + }, + { + "in": 1, + "match": null, + "at": -1 + }, + { + "in": 2, + "match": "(", + "at": 9 + }, + { + "in": 3, + "match": "&", + "at": 6 + }, + { + "in": 4, + "match": null, + "at": -1 + }, + { + "in": 5, + "match": "!", + "at": 13 + }, + { + "in": 6, + "match": ",", + "at": 14 + }, + { + "in": 7, + "match": null, + "at": -1 + }, + { + "in": 8, + "match": "\"", + "at": 17 + }, + { + "in": 9, + "match": null, + "at": -1 + }, + { + "in": 10, + "match": "*+", + "at": 2 + } + ] + }, + { + "pattern": "[a--]", + "compiles": false, + "matches": [] + }, + { + "pattern": "[~~a]+", + "compiles": true, + "matches": [ + { + "in": 0, + "match": "a", + "at": 26 + }, + { + "in": 1, + "match": "a", + "at": 6 + }, + { + "in": 2, + "match": "a", + "at": 0 + }, + { + "in": 3, + "match": "a", + "at": 2 + }, + { + "in": 4, + "match": "a", + "at": 1 + }, + { + "in": 5, + "match": "a", + "at": 16 + }, + { + "in": 6, + "match": "a", + "at": 5 + }, + { + "in": 7, + "match": "aaaa", + "at": 0 + }, + { + "in": 8, + "match": "a", + "at": 11 + }, + { + "in": 9, + "match": "A", + "at": 0 + }, + { + "in": 10, + "match": null, + "at": -1 + } + ] + }, + { + "pattern": "[]a]+", + "compiles": true, + "matches": [ + { + "in": 0, + "match": "a", + "at": 26 + }, + { + "in": 1, + "match": "a", + "at": 6 + }, + { + "in": 2, + "match": "a", + "at": 0 + }, + { + "in": 3, + "match": "a", + "at": 2 + }, + { + "in": 4, + "match": "a", + "at": 1 + }, + { + "in": 5, + "match": "a", + "at": 16 + }, + { + "in": 6, + "match": "a", + "at": 5 + }, + { + "in": 7, + "match": "aaaa", + "at": 0 + }, + { + "in": 8, + "match": "a", + "at": 11 + }, + { + "in": 9, + "match": "A", + "at": 0 + }, + { + "in": 10, + "match": null, + "at": -1 + } + ] + }, + { + "pattern": "[^]a]+", + "compiles": true, + "matches": [ + { + "in": 0, + "match": "été do_it(1) éfoo foo b", + "at": 0 + }, + { + "in": 1, + "match": "x٣42 ", + "at": 0 + }, + { + "in": 2, + "match": "xb ", + "at": 1 + }, + { + "in": 3, + "match": "x[", + "at": 0 + }, + { + "in": 4, + "match": "1", + "at": 0 + }, + { + "in": 5, + "match": "Bonjour\nOUPS !\nl", + "at": 0 + }, + { + "in": 6, + "match": "oups ", + "at": 0 + }, + { + "in": 7, + "match": " xxx ", + "at": 4 + }, + { + "in": 8, + "match": " {\"name\": \"read\"}", + "at": 0 + }, + { + "in": 9, + "match": "AB ABC éé ÉTÉ été", + "at": 0 + }, + { + "in": 10, + "match": "x.*+y", + "at": 0 + } + ] + }, + { + "pattern": "^oups", + "compiles": true, + "matches": [ + { + "in": 0, + "match": null, + "at": -1 + }, + { + "in": 1, + "match": null, + "at": -1 + }, + { + "in": 2, + "match": null, + "at": -1 + }, + { + "in": 3, + "match": null, + "at": -1 + }, + { + "in": 4, + "match": null, + "at": -1 + }, + { + "in": 5, + "match": "OUPS", + "at": 8 + }, + { + "in": 6, + "match": "oups", + "at": 0 + }, + { + "in": 7, + "match": null, + "at": -1 + }, + { + "in": 8, + "match": null, + "at": -1 + }, + { + "in": 9, + "match": null, + "at": -1 + }, + { + "in": 10, + "match": null, + "at": -1 + } + ] + }, + { + "pattern": "\\Aoups", + "compiles": true, + "matches": [ + { + "in": 0, + "match": null, + "at": -1 + }, + { + "in": 1, + "match": null, + "at": -1 + }, + { + "in": 2, + "match": null, + "at": -1 + }, + { + "in": 3, + "match": null, + "at": -1 + }, + { + "in": 4, + "match": null, + "at": -1 + }, + { + "in": 5, + "match": null, + "at": -1 + }, + { + "in": 6, + "match": "oups", + "at": 0 + }, + { + "in": 7, + "match": null, + "at": -1 + }, + { + "in": 8, + "match": null, + "at": -1 + }, + { + "in": 9, + "match": null, + "at": -1 + }, + { + "in": 10, + "match": null, + "at": -1 + } + ] + }, + { + "pattern": "fin$", + "compiles": true, + "matches": [ + { + "in": 0, + "match": null, + "at": -1 + }, + { + "in": 1, + "match": null, + "at": -1 + }, + { + "in": 2, + "match": null, + "at": -1 + }, + { + "in": 3, + "match": null, + "at": -1 + }, + { + "in": 4, + "match": null, + "at": -1 + }, + { + "in": 5, + "match": "fin", + "at": 18 + }, + { + "in": 6, + "match": "FIN", + "at": 24 + }, + { + "in": 7, + "match": null, + "at": -1 + }, + { + "in": 8, + "match": null, + "at": -1 + }, + { + "in": 9, + "match": null, + "at": -1 + }, + { + "in": 10, + "match": null, + "at": -1 + } + ] + }, + { + "pattern": "\\pL+", + "compiles": true, + "matches": [ + { + "in": 0, + "match": "été", + "at": 0 + }, + { + "in": 1, + "match": "x", + "at": 0 + }, + { + "in": 2, + "match": "axb", + "at": 0 + }, + { + "in": 3, + "match": "x", + "at": 0 + }, + { + "in": 4, + "match": "abc", + "at": 1 + }, + { + "in": 5, + "match": "Bonjour", + "at": 0 + }, + { + "in": 6, + "match": "oups", + "at": 0 + }, + { + "in": 7, + "match": "aaaa", + "at": 0 + }, + { + "in": 8, + "match": "function", + "at": 1 + }, + { + "in": 9, + "match": "AB", + "at": 0 + }, + { + "in": 10, + "match": "x", + "at": 0 + } + ] + }, + { + "pattern": "\\p{Greek}+", + "compiles": true, + "matches": [ + { + "in": 0, + "match": null, + "at": -1 + }, + { + "in": 1, + "match": null, + "at": -1 + }, + { + "in": 2, + "match": null, + "at": -1 + }, + { + "in": 3, + "match": null, + "at": -1 + }, + { + "in": 4, + "match": "Ωμέγα", + "at": 10 + }, + { + "in": 5, + "match": null, + "at": -1 + }, + { + "in": 6, + "match": null, + "at": -1 + }, + { + "in": 7, + "match": null, + "at": -1 + }, + { + "in": 8, + "match": null, + "at": -1 + }, + { + "in": 9, + "match": null, + "at": -1 + }, + { + "in": 10, + "match": null, + "at": -1 + } + ] + }, + { + "pattern": "x{2,3}", + "compiles": true, + "matches": [ + { + "in": 0, + "match": null, + "at": -1 + }, + { + "in": 1, + "match": null, + "at": -1 + }, + { + "in": 2, + "match": null, + "at": -1 + }, + { + "in": 3, + "match": null, + "at": -1 + }, + { + "in": 4, + "match": null, + "at": -1 + }, + { + "in": 5, + "match": null, + "at": -1 + }, + { + "in": 6, + "match": null, + "at": -1 + }, + { + "in": 7, + "match": "xxx", + "at": 5 + }, + { + "in": 8, + "match": null, + "at": -1 + }, + { + "in": 9, + "match": null, + "at": -1 + }, + { + "in": 10, + "match": null, + "at": -1 + } + ] + }, + { + "pattern": "(?s:a.b)", + "compiles": true, + "matches": [ + { + "in": 0, + "match": null, + "at": -1 + }, + { + "in": 1, + "match": null, + "at": -1 + }, + { + "in": 2, + "match": "axb", + "at": 0 + }, + { + "in": 3, + "match": null, + "at": -1 + }, + { + "in": 4, + "match": null, + "at": -1 + }, + { + "in": 5, + "match": null, + "at": -1 + }, + { + "in": 6, + "match": null, + "at": -1 + }, + { + "in": 7, + "match": "a\nb", + "at": 9 + }, + { + "in": 8, + "match": null, + "at": -1 + }, + { + "in": 9, + "match": null, + "at": -1 + }, + { + "in": 10, + "match": null, + "at": -1 + } + ] + }, + { + "pattern": "a.b", + "compiles": true, + "matches": [ + { + "in": 0, + "match": null, + "at": -1 + }, + { + "in": 1, + "match": null, + "at": -1 + }, + { + "in": 2, + "match": "axb", + "at": 0 + }, + { + "in": 3, + "match": null, + "at": -1 + }, + { + "in": 4, + "match": null, + "at": -1 + }, + { + "in": 5, + "match": null, + "at": -1 + }, + { + "in": 6, + "match": null, + "at": -1 + }, + { + "in": 7, + "match": "aéb", + "at": 13 + }, + { + "in": 8, + "match": null, + "at": -1 + }, + { + "in": 9, + "match": null, + "at": -1 + }, + { + "in": 10, + "match": null, + "at": -1 + } + ] + }, + { + "pattern": "(?U)a+", + "compiles": true, + "matches": [ + { + "in": 0, + "match": "a", + "at": 26 + }, + { + "in": 1, + "match": "a", + "at": 6 + }, + { + "in": 2, + "match": "a", + "at": 0 + }, + { + "in": 3, + "match": "a", + "at": 2 + }, + { + "in": 4, + "match": "a", + "at": 1 + }, + { + "in": 5, + "match": "a", + "at": 16 + }, + { + "in": 6, + "match": "a", + "at": 5 + }, + { + "in": 7, + "match": "a", + "at": 0 + }, + { + "in": 8, + "match": "a", + "at": 11 + }, + { + "in": 9, + "match": "A", + "at": 0 + }, + { + "in": 10, + "match": null, + "at": -1 + } + ] + }, + { + "pattern": "", + "compiles": true, + "matches": [ + { + "in": 0, + "match": null, + "at": -1 + }, + { + "in": 1, + "match": null, + "at": -1 + }, + { + "in": 2, + "match": null, + "at": -1 + }, + { + "in": 3, + "match": null, + "at": -1 + }, + { + "in": 4, + "match": null, + "at": -1 + }, + { + "in": 5, + "match": null, + "at": -1 + }, + { + "in": 6, + "match": null, + "at": -1 + }, + { + "in": 7, + "match": null, + "at": -1 + }, + { + "in": 8, + "match": "", + "at": 0 + }, + { + "in": 9, + "match": null, + "at": -1 + }, + { + "in": 10, + "match": null, + "at": -1 + } + ] + }, + { + "pattern": "\\{\"name\":\\s*\"(bash|read)\"", + "compiles": true, + "matches": [ + { + "in": 0, + "match": null, + "at": -1 + }, + { + "in": 1, + "match": null, + "at": -1 + }, + { + "in": 2, + "match": null, + "at": -1 + }, + { + "in": 3, + "match": null, + "at": -1 + }, + { + "in": 4, + "match": null, + "at": -1 + }, + { + "in": 5, + "match": null, + "at": -1 + }, + { + "in": 6, + "match": null, + "at": -1 + }, + { + "in": 7, + "match": null, + "at": -1 + }, + { + "in": 8, + "match": "{\"name\": \"read\"", + "at": 16 + }, + { + "in": 9, + "match": null, + "at": -1 + }, + { + "in": 10, + "match": null, + "at": -1 + } + ] + }, + { + "pattern": "[", + "compiles": false, + "matches": [] + }, + { + "pattern": "(", + "compiles": false, + "matches": [] + }, + { + "pattern": "\\C", + "compiles": false, + "matches": [] + }, + { + "pattern": "\\z", + "compiles": true, + "matches": [ + { + "in": 0, + "match": "", + "at": 30 + }, + { + "in": 1, + "match": "", + "at": 13 + }, + { + "in": 2, + "match": "", + "at": 17 + }, + { + "in": 3, + "match": "", + "at": 27 + }, + { + "in": 4, + "match": "", + "at": 20 + }, + { + "in": 5, + "match": "", + "at": 21 + }, + { + "in": 6, + "match": "", + "at": 27 + }, + { + "in": 7, + "match": "", + "at": 17 + }, + { + "in": 8, + "match": "", + "at": 32 + }, + { + "in": 9, + "match": "", + "at": 23 + }, + { + "in": 10, + "match": "", + "at": 5 + } + ] + }, + { + "pattern": "a\\z", + "compiles": true, + "matches": [ + { + "in": 0, + "match": null, + "at": -1 + }, + { + "in": 1, + "match": null, + "at": -1 + }, + { + "in": 2, + "match": null, + "at": -1 + }, + { + "in": 3, + "match": null, + "at": -1 + }, + { + "in": 4, + "match": null, + "at": -1 + }, + { + "in": 5, + "match": null, + "at": -1 + }, + { + "in": 6, + "match": null, + "at": -1 + }, + { + "in": 7, + "match": null, + "at": -1 + }, + { + "in": 8, + "match": null, + "at": -1 + }, + { + "in": 9, + "match": null, + "at": -1 + }, + { + "in": 10, + "match": null, + "at": -1 + } + ] + }, + { + "pattern": "(?Pab)", + "compiles": true, + "matches": [ + { + "in": 0, + "match": null, + "at": -1 + }, + { + "in": 1, + "match": null, + "at": -1 + }, + { + "in": 2, + "match": null, + "at": -1 + }, + { + "in": 3, + "match": "ab", + "at": 7 + }, + { + "in": 4, + "match": "ab", + "at": 1 + }, + { + "in": 5, + "match": null, + "at": -1 + }, + { + "in": 6, + "match": null, + "at": -1 + }, + { + "in": 7, + "match": null, + "at": -1 + }, + { + "in": 8, + "match": null, + "at": -1 + }, + { + "in": 9, + "match": "AB", + "at": 0 + }, + { + "in": 10, + "match": null, + "at": -1 + } + ] + }, + { + "pattern": "\\x41\\x{42}", + "compiles": true, + "matches": [ + { + "in": 0, + "match": null, + "at": -1 + }, + { + "in": 1, + "match": null, + "at": -1 + }, + { + "in": 2, + "match": null, + "at": -1 + }, + { + "in": 3, + "match": "ab", + "at": 7 + }, + { + "in": 4, + "match": "ab", + "at": 1 + }, + { + "in": 5, + "match": null, + "at": -1 + }, + { + "in": 6, + "match": null, + "at": -1 + }, + { + "in": 7, + "match": null, + "at": -1 + }, + { + "in": 8, + "match": null, + "at": -1 + }, + { + "in": 9, + "match": "AB", + "at": 0 + }, + { + "in": 10, + "match": null, + "at": -1 + } + ] + }, + { + "pattern": "\\101", + "compiles": true, + "matches": [ + { + "in": 0, + "match": "a", + "at": 26 + }, + { + "in": 1, + "match": "a", + "at": 6 + }, + { + "in": 2, + "match": "a", + "at": 0 + }, + { + "in": 3, + "match": "a", + "at": 2 + }, + { + "in": 4, + "match": "a", + "at": 1 + }, + { + "in": 5, + "match": "a", + "at": 16 + }, + { + "in": 6, + "match": "a", + "at": 5 + }, + { + "in": 7, + "match": "a", + "at": 0 + }, + { + "in": 8, + "match": "a", + "at": 11 + }, + { + "in": 9, + "match": "A", + "at": 0 + }, + { + "in": 10, + "match": null, + "at": -1 + } + ] + }, + { + "pattern": "\\1", + "compiles": false, + "matches": [] + }, + { + "pattern": "(a)\\1", + "compiles": false, + "matches": [] + }, + { + "pattern": "\\0", + "compiles": true, + "matches": [ + { + "in": 0, + "match": null, + "at": -1 + }, + { + "in": 1, + "match": null, + "at": -1 + }, + { + "in": 2, + "match": null, + "at": -1 + }, + { + "in": 3, + "match": null, + "at": -1 + }, + { + "in": 4, + "match": null, + "at": -1 + }, + { + "in": 5, + "match": null, + "at": -1 + }, + { + "in": 6, + "match": null, + "at": -1 + }, + { + "in": 7, + "match": null, + "at": -1 + }, + { + "in": 8, + "match": null, + "at": -1 + }, + { + "in": 9, + "match": null, + "at": -1 + }, + { + "in": 10, + "match": null, + "at": -1 + } + ] + }, + { + "pattern": "\\08", + "compiles": true, + "matches": [ + { + "in": 0, + "match": null, + "at": -1 + }, + { + "in": 1, + "match": null, + "at": -1 + }, + { + "in": 2, + "match": null, + "at": -1 + }, + { + "in": 3, + "match": null, + "at": -1 + }, + { + "in": 4, + "match": null, + "at": -1 + }, + { + "in": 5, + "match": null, + "at": -1 + }, + { + "in": 6, + "match": null, + "at": -1 + }, + { + "in": 7, + "match": null, + "at": -1 + }, + { + "in": 8, + "match": null, + "at": -1 + }, + { + "in": 9, + "match": null, + "at": -1 + }, + { + "in": 10, + "match": null, + "at": -1 + } + ] + }, + { + "pattern": "[\\101-\\103]+", + "compiles": true, + "matches": [ + { + "in": 0, + "match": "ba", + "at": 25 + }, + { + "in": 1, + "match": "a", + "at": 6 + }, + { + "in": 2, + "match": "a", + "at": 0 + }, + { + "in": 3, + "match": "a", + "at": 2 + }, + { + "in": 4, + "match": "abc", + "at": 1 + }, + { + "in": 5, + "match": "B", + "at": 0 + }, + { + "in": 6, + "match": "a", + "at": 5 + }, + { + "in": 7, + "match": "aaaa", + "at": 0 + }, + { + "in": 8, + "match": "c", + "at": 4 + }, + { + "in": 9, + "match": "AB", + "at": 0 + }, + { + "in": 10, + "match": null, + "at": -1 + } + ] + }, + { + "pattern": "[\\x41-\\x43]+", + "compiles": true, + "matches": [ + { + "in": 0, + "match": "ba", + "at": 25 + }, + { + "in": 1, + "match": "a", + "at": 6 + }, + { + "in": 2, + "match": "a", + "at": 0 + }, + { + "in": 3, + "match": "a", + "at": 2 + }, + { + "in": 4, + "match": "abc", + "at": 1 + }, + { + "in": 5, + "match": "B", + "at": 0 + }, + { + "in": 6, + "match": "a", + "at": 5 + }, + { + "in": 7, + "match": "aaaa", + "at": 0 + }, + { + "in": 8, + "match": "c", + "at": 4 + }, + { + "in": 9, + "match": "AB", + "at": 0 + }, + { + "in": 10, + "match": null, + "at": -1 + } + ] + }, + { + "pattern": "\\.\\*\\+", + "compiles": true, + "matches": [ + { + "in": 0, + "match": null, + "at": -1 + }, + { + "in": 1, + "match": null, + "at": -1 + }, + { + "in": 2, + "match": null, + "at": -1 + }, + { + "in": 3, + "match": null, + "at": -1 + }, + { + "in": 4, + "match": null, + "at": -1 + }, + { + "in": 5, + "match": null, + "at": -1 + }, + { + "in": 6, + "match": null, + "at": -1 + }, + { + "in": 7, + "match": null, + "at": -1 + }, + { + "in": 8, + "match": null, + "at": -1 + }, + { + "in": 9, + "match": null, + "at": -1 + }, + { + "in": 10, + "match": ".*+", + "at": 1 + } + ] + }, + { + "pattern": "é+", + "compiles": true, + "matches": [ + { + "in": 0, + "match": "é", + "at": 0 + }, + { + "in": 1, + "match": null, + "at": -1 + }, + { + "in": 2, + "match": null, + "at": -1 + }, + { + "in": 3, + "match": null, + "at": -1 + }, + { + "in": 4, + "match": null, + "at": -1 + }, + { + "in": 5, + "match": null, + "at": -1 + }, + { + "in": 6, + "match": "é", + "at": 9 + }, + { + "in": 7, + "match": "é", + "at": 14 + }, + { + "in": 8, + "match": null, + "at": -1 + }, + { + "in": 9, + "match": "éé", + "at": 7 + }, + { + "in": 10, + "match": null, + "at": -1 + } + ] + }, + { + "pattern": "ÉTÉ", + "compiles": true, + "matches": [ + { + "in": 0, + "match": "été", + "at": 0 + }, + { + "in": 1, + "match": null, + "at": -1 + }, + { + "in": 2, + "match": null, + "at": -1 + }, + { + "in": 3, + "match": null, + "at": -1 + }, + { + "in": 4, + "match": null, + "at": -1 + }, + { + "in": 5, + "match": null, + "at": -1 + }, + { + "in": 6, + "match": null, + "at": -1 + }, + { + "in": 7, + "match": null, + "at": -1 + }, + { + "in": 8, + "match": null, + "at": -1 + }, + { + "in": 9, + "match": "ÉTÉ", + "at": 12 + }, + { + "in": 10, + "match": null, + "at": -1 + } + ] + } + ] +} diff --git a/rust/loki-rs/tests/retry.rs b/rust/loki-rs/tests/retry.rs new file mode 100644 index 0000000..a2d2c03 --- /dev/null +++ b/rust/loki-rs/tests/retry.rs @@ -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é")); + } + } +}