assay_core/agent_assertions/model.rs
1use serde::{Deserialize, Serialize};
2
3/// An assertion carrying a key this enum does not define is rejected at parse time.
4///
5/// Without `deny_unknown_fields` serde drops the unrecognised key silently, and where the
6/// intended field has a default the assertion falls back to a shape that cannot fail. The
7/// documented `max_calls: 0` "must NOT use a forbidden tool" example is the worked case: the
8/// key is dropped, `min_calls` defaults to 1 in `matchers.rs`, and the assertion inverts into
9/// "must be called at least once" with no signal at any stage (#1961).
10///
11/// `deny_unknown_fields` is applied at the **container**, which is the only place serde accepts
12/// it — it is not a variant attribute, and the compiler rejects it as one. There is folklore
13/// that container-level rejection is unreliable on an internally-tagged enum (serde-rs/serde
14/// #2294, #1358). Those defects are about unit-like variants and `flatten`; every variant here
15/// is a struct variant with named fields and none flattens, and rejection was verified on this
16/// exact shape for the hardest case in it — `tool_blocklist`, whose fields are all defaulted, so
17/// nothing but the tag is required. A stray key there is rejected too. Nested free-form values
18/// (`policy`, `test_args`) are `serde_json::Value` and stay unconstrained, which is intended:
19/// the guard covers the assertion's own field vocabulary, not policy contents.
20///
21/// Keep the guard at the container. Per-variant allow-set validation was considered and is not
22/// needed here; the tests in `tests/assertions_unknown_fields.rs` pin the behaviour that makes
23/// it unnecessary, including the all-defaulted variant.
24#[derive(Debug, Clone, Serialize, Deserialize)]
25#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
26pub enum TraceAssertion {
27 #[serde(rename = "trace_must_call_tool")]
28 TraceMustCallTool {
29 tool: String,
30 min_calls: Option<u32>,
31 },
32 #[serde(rename = "trace_must_not_call_tool")]
33 TraceMustNotCallTool { tool: String },
34 #[serde(rename = "trace_tool_sequence")]
35 TraceToolSequence {
36 sequence: Vec<String>,
37 allow_other_tools: bool,
38 },
39 #[serde(rename = "trace_max_steps")]
40 TraceMaxSteps { max: u32 },
41 #[serde(rename = "args_valid")]
42 ArgsValid {
43 tool: String,
44 #[serde(default)]
45 test_args: Option<serde_json::Value>,
46 #[serde(default)]
47 policy: Option<serde_json::Value>,
48 #[serde(default)]
49 expect: Option<String>,
50 },
51 #[serde(rename = "sequence_valid")]
52 SequenceValid {
53 #[serde(default)]
54 test_trace: Option<Vec<crate::storage::rows::ToolCallRow>>, // Reusing existing struct or simplified Value
55 // If the user uses simplified structure in yaml, we might need a custom struct or Value.
56 // fp_suite uses: - tool: VerifyIdentity, args: {}
57 // ToolCallRow is a bit heavy, let's use Value for flexibility if model mismatch.
58 // But for safety, let's look at strict parsing.
59 // Example: { tool: "VerifyIdentity", args: {} }
60 #[serde(default)]
61 test_trace_raw: Option<Vec<serde_json::Value>>,
62 #[serde(default)]
63 policy: Option<serde_json::Value>,
64 #[serde(default)]
65 expect: Option<String>,
66 },
67 #[serde(rename = "tool_blocklist")]
68 ToolBlocklist {
69 #[serde(default)]
70 test_tool_calls: Option<Vec<String>>,
71 #[serde(default)]
72 policy: Option<serde_json::Value>,
73 #[serde(default)]
74 expect: Option<String>,
75 },
76}