assay_core/agent_assertions/
mod.rs

1pub mod matchers;
2pub mod model;
3
4use crate::errors::diagnostic::Diagnostic;
5use crate::storage::Store;
6
7pub struct EpisodeGraph {
8    pub episode_id: String,
9    pub steps: Vec<crate::storage::rows::StepRow>,
10    pub tool_calls: Vec<crate::storage::rows::ToolCallRow>,
11}
12
13pub fn verify_assertions(
14    store: &Store,
15    run_id: i64,
16    test_id: &str,
17    assertions: &[model::TraceAssertion],
18) -> anyhow::Result<Vec<Diagnostic>> {
19    let graph_res = store.get_episode_graph(run_id, test_id);
20    match graph_res {
21        Ok(graph) => matchers::evaluate(&graph, assertions),
22        Err(e) => {
23            // FALLBACK 1: Unit Test Mode (Policy Validation)
24            // If assertions have explicit `test_args`, `test_trace`, etc., we don't need a real episode.
25            // Check if ALL assertions are unit tests.
26            let is_unit_test = assertions.iter().all(|a| match a {
27                model::TraceAssertion::ArgsValid { test_args, .. } => test_args.is_some(),
28                model::TraceAssertion::SequenceValid {
29                    test_trace,
30                    test_trace_raw,
31                    ..
32                } => test_trace.is_some() || test_trace_raw.is_some(),
33                model::TraceAssertion::ToolBlocklist {
34                    test_tool_calls, ..
35                } => test_tool_calls.is_some(),
36                _ => false,
37            });
38
39            if is_unit_test {
40                // Construct dummy graph
41                let dummy = EpisodeGraph {
42                    episode_id: "unit_test_mock".into(),
43                    steps: vec![],
44                    tool_calls: vec![],
45                };
46                return matchers::evaluate(&dummy, assertions);
47            }
48
49            // FALLBACK 2 (PR-406): If no episode found for this run_id,
50            // try to find the LATEST episode for this test_id regardless of run_id.
51            // This supports the "Demo Flow": Record -> Ingest (Run A) -> Verify (Run B)
52            if e.to_string().contains("E_TRACE_EPISODE_MISSING") {
53                match store.get_latest_episode_graph_by_test_id(test_id) {
54                    Ok(latest_graph) => return matchers::evaluate(&latest_graph, assertions),
55                    Err(fallback_err) => {
56                        return Err(anyhow::anyhow!("E_TRACE_EPISODE_MISSING: Primary query failed ({}), Fallback failed: {}", e, fallback_err));
57                    }
58                }
59            }
60
61            // Check if error is ambiguous or missing
62            // For now, return Err to platform, but ideally convert to Diagnostic
63            Err(e)
64        }
65    }
66}