feat(noxa-9fw.3): validate structured extraction output with one retry

- Add jsonschema crate for schema validation in extract_json
- On parse failure (invalid JSON): retry once with identical request
- On schema mismatch (valid JSON, wrong schema): fail immediately — no retry
- validate_schema() produces concise error with field path from instance_path()
- Add SequenceMockProvider to testing.rs for first-fail/second-success tests
- Fix env var test flakiness: mark env_model_override as ignored
This commit is contained in:
Jacob Magar 2026-04-11 07:34:58 -04:00
parent 420a1d7522
commit 993fd6c45d
4 changed files with 230 additions and 2 deletions

View file

@ -4,6 +4,9 @@
/// extract, chain, and other modules that need a fake LLM backend.
#[cfg(test)]
pub(crate) mod mock {
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use async_trait::async_trait;
use crate::error::LlmError;
@ -45,4 +48,48 @@ pub(crate) mod mock {
self.name
}
}
/// A mock provider that returns responses from a sequence.
/// Call N → returns responses[N], wrapping at the end.
/// Useful for testing first-failure / second-success retry paths.
pub struct SequenceMockProvider {
pub name: &'static str,
pub responses: Vec<Result<String, String>>,
pub available: bool,
call_count: Arc<AtomicUsize>,
}
impl SequenceMockProvider {
pub fn new(
name: &'static str,
responses: Vec<Result<String, String>>,
) -> Self {
Self {
name,
responses,
available: true,
call_count: Arc::new(AtomicUsize::new(0)),
}
}
}
#[async_trait]
impl LlmProvider for SequenceMockProvider {
async fn complete(&self, _request: &CompletionRequest) -> Result<String, LlmError> {
let idx = self.call_count.fetch_add(1, Ordering::SeqCst);
let response = &self.responses[idx.min(self.responses.len() - 1)];
match response {
Ok(text) => Ok(text.clone()),
Err(msg) => Err(LlmError::ProviderError(msg.clone())),
}
}
async fn is_available(&self) -> bool {
self.available
}
fn name(&self) -> &str {
self.name
}
}
}