Skip to main content

chio_conformance/
native_suite.rs

1use std::fs;
2use std::io::{Read, Write};
3use std::path::{Path, PathBuf};
4use std::process::{Command, Stdio};
5use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
6
7use chio_core::canonical::canonical_json_bytes;
8use chio_core::capability::{
9    attenuation::{
10        scope_hash, validate_attenuation, validate_delegation_chain, Attenuation, DelegationLink,
11        DelegationLinkBody,
12    },
13    governance::GovernedTransactionIntent,
14    scope::{ChioScope, Constraint, Operation, ToolGrant},
15    token::{CapabilityToken, CapabilityTokenBody},
16};
17use chio_core::crypto::Keypair;
18use chio_core::message::{AgentMessage, KernelMessage, ToolCallError, ToolCallResult};
19use chio_core::receipt::{
20    body::ChioReceipt, body::ChioReceiptBody, decision::Decision, decision::ToolCallAction,
21    governance::GovernedTransactionReceiptMetadata, kinds::BoundaryClass, kinds::ReceiptKind,
22    kinds::RedactionMode, kinds::ToolOrigin, metadata::GuardEvidence,
23};
24use chio_kernel::dpop::{verify_dpop_proof, DpopConfig, DpopNonceStore, DpopProof, DpopProofBody};
25use chio_kernel::transport::{read_frame, write_frame, TransportError};
26use reqwest::blocking::Client;
27use serde::{Deserialize, Serialize};
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
30#[serde(rename_all = "snake_case")]
31pub enum NativeDriver {
32    Artifact,
33    Stdio,
34    Http,
35}
36
37impl NativeDriver {
38    pub fn label(self) -> &'static str {
39        match self {
40            Self::Artifact => "artifact",
41            Self::Stdio => "stdio",
42            Self::Http => "http",
43        }
44    }
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
48#[serde(rename_all = "snake_case")]
49pub enum NativeScenarioCategory {
50    CapabilityValidation,
51    DelegationAttenuation,
52    ReceiptIntegrity,
53    RevocationPropagation,
54    DpopVerification,
55    GovernedTransactionEnforcement,
56}
57
58impl NativeScenarioCategory {
59    pub fn heading(self) -> &'static str {
60        match self {
61            Self::CapabilityValidation => "Capability Validation",
62            Self::DelegationAttenuation => "Delegation Attenuation",
63            Self::ReceiptIntegrity => "Receipt Integrity",
64            Self::RevocationPropagation => "Revocation Propagation",
65            Self::DpopVerification => "DPoP Verification",
66            Self::GovernedTransactionEnforcement => "Governed Transaction Enforcement",
67        }
68    }
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
72#[serde(rename_all = "snake_case")]
73pub enum NativeAssertionKind {
74    CapabilitySignatureValid,
75    DelegationChainValid,
76    DelegationAttenuatesParent,
77    ReceiptSignatureValid,
78    ReceiptTamperRejected,
79    DpopProofValid,
80    TerminalStatus,
81    ToolErrorCode,
82    ResponseReceiptSignatureValid,
83    GovernedReceiptPresent,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
87#[serde(rename_all = "camelCase")]
88pub struct NativeAssertionSpec {
89    pub name: String,
90    pub kind: NativeAssertionKind,
91    #[serde(default)]
92    pub expected_bool: Option<bool>,
93    #[serde(default)]
94    pub expected_string: Option<String>,
95}
96
97#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
98#[serde(rename_all = "camelCase")]
99pub struct NativeScenarioDescriptor {
100    pub id: String,
101    pub title: String,
102    pub category: NativeScenarioCategory,
103    pub driver: NativeDriver,
104    pub fixture: String,
105    pub spec_version: String,
106    pub assertions: Vec<NativeAssertionSpec>,
107    #[serde(default)]
108    pub notes: Option<String>,
109    #[serde(default)]
110    pub http_path: Option<String>,
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
114#[serde(rename_all = "kebab-case")]
115pub enum NativeStatus {
116    Pass,
117    Fail,
118}
119
120impl NativeStatus {
121    pub fn label(self) -> &'static str {
122        match self {
123            Self::Pass => "pass",
124            Self::Fail => "fail",
125        }
126    }
127}
128
129#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
130#[serde(rename_all = "camelCase")]
131pub struct NativeAssertionResult {
132    pub name: String,
133    pub status: NativeStatus,
134    #[serde(default)]
135    pub message: Option<String>,
136}
137
138#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
139#[serde(rename_all = "camelCase")]
140pub struct NativeScenarioResult {
141    pub scenario_id: String,
142    pub title: String,
143    pub category: NativeScenarioCategory,
144    pub driver: NativeDriver,
145    pub spec_version: String,
146    pub status: NativeStatus,
147    pub duration_ms: u64,
148    pub assertions: Vec<NativeAssertionResult>,
149    #[serde(default)]
150    pub notes: Option<String>,
151    #[serde(default)]
152    pub failure_message: Option<String>,
153}
154
155#[derive(Debug, Clone)]
156pub struct NativeConformanceRunOptions {
157    pub repo_root: PathBuf,
158    pub scenarios_dir: PathBuf,
159    pub results_output: PathBuf,
160    pub report_output: PathBuf,
161    pub peer_label: String,
162    pub stdio_command: Option<PathBuf>,
163    pub http_base_url: Option<String>,
164}
165
166#[derive(Debug, Clone)]
167pub struct NativeConformanceRunSummary {
168    pub scenario_count: usize,
169    pub results_output: PathBuf,
170    pub report_output: PathBuf,
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize)]
174#[serde(rename_all = "camelCase")]
175pub struct NativeFixtureRequest {
176    pub scenario_id: String,
177    pub request: AgentMessage,
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize)]
181#[serde(rename_all = "camelCase")]
182pub struct NativeFixtureResponse {
183    pub messages: Vec<KernelMessage>,
184}
185
186#[derive(Debug, thiserror::Error)]
187pub enum NativeSuiteError {
188    #[error("i/o error: {0}")]
189    Io(#[from] std::io::Error),
190
191    #[error("json error in {path}: {source}")]
192    Json {
193        path: String,
194        #[source]
195        source: serde_json::Error,
196    },
197
198    #[error("http driver error: {0}")]
199    Http(String),
200
201    #[error("transport error: {0}")]
202    Transport(#[from] TransportError),
203
204    #[error("fixture `{0}` is not known")]
205    UnknownFixture(String),
206
207    #[error("scenario `{scenario}` requires a stdio command")]
208    MissingStdioCommand { scenario: String },
209
210    #[error("scenario `{scenario}` requires an http base url")]
211    MissingHttpBaseUrl { scenario: String },
212
213    #[error("scenario `{scenario}` produced no terminal response")]
214    MissingTerminalResponse { scenario: String },
215}
216
217pub fn default_native_run_options() -> NativeConformanceRunOptions {
218    let repo_root = super::default_repo_root();
219    NativeConformanceRunOptions {
220        scenarios_dir: repo_root.join("tests/conformance/native/scenarios"),
221        results_output: repo_root.join("tests/conformance/native/results/generated/chio-self.json"),
222        report_output: repo_root.join("tests/conformance/native/reports/generated/chio-self.md"),
223        peer_label: "chio-self".to_string(),
224        stdio_command: None,
225        http_base_url: None,
226        repo_root,
227    }
228}
229
230pub fn run_native_conformance_suite(
231    options: &NativeConformanceRunOptions,
232) -> Result<NativeConformanceRunSummary, NativeSuiteError> {
233    let scenarios = load_native_scenarios_from_dir(&options.scenarios_dir)?;
234    if let Some(parent) = options.results_output.parent() {
235        fs::create_dir_all(parent)?;
236    }
237    if let Some(parent) = options.report_output.parent() {
238        fs::create_dir_all(parent)?;
239    }
240
241    let mut results = Vec::new();
242    for scenario in &scenarios {
243        results.push(execute_native_scenario(scenario, options)?);
244    }
245
246    fs::write(
247        &options.results_output,
248        serde_json::to_string_pretty(&results).map_err(|source| NativeSuiteError::Json {
249            path: options.results_output.display().to_string(),
250            source,
251        })?,
252    )?;
253    fs::write(
254        &options.report_output,
255        generate_native_markdown_report(&results),
256    )?;
257
258    Ok(NativeConformanceRunSummary {
259        scenario_count: results.len(),
260        results_output: options.results_output.clone(),
261        report_output: options.report_output.clone(),
262    })
263}
264
265pub fn load_native_scenarios_from_dir(
266    path: impl AsRef<Path>,
267) -> Result<Vec<NativeScenarioDescriptor>, NativeSuiteError> {
268    let mut scenarios = Vec::new();
269    let path = path.as_ref();
270    require_native_scenario_directory(path)?;
271    collect_native_scenarios(path, &mut scenarios)?;
272    if scenarios.is_empty() {
273        return Err(empty_native_scenario_directory_error(path));
274    }
275    scenarios.sort_by(|left, right| left.id.cmp(&right.id));
276    Ok(scenarios)
277}
278
279#[allow(clippy::expect_used)]
280pub fn fixture_messages_for_request(request: &AgentMessage) -> Vec<KernelMessage> {
281    match request {
282        AgentMessage::Heartbeat => vec![KernelMessage::Heartbeat],
283        AgentMessage::ListCapabilities => vec![KernelMessage::CapabilityList {
284            capabilities: vec![build_valid_capability()],
285        }],
286        AgentMessage::ToolCallRequest {
287            id,
288            capability_token,
289            tool,
290            params,
291            ..
292        } if capability_token.id == "cap-revoked-001" => {
293            vec![KernelMessage::ToolCallResponse {
294                id: id.clone(),
295                result: ToolCallResult::Err {
296                    error: ToolCallError::CapabilityRevoked,
297                },
298                receipt: Box::new(build_receipt(
299                    "rcpt-revoked-001",
300                    &capability_token.id,
301                    tool,
302                    params.as_ref().clone(),
303                    Decision::Deny {
304                        reason: "capability revoked".to_string(),
305                        guard: "revocation_store".to_string(),
306                    },
307                    None,
308                )),
309                execution_nonce: None,
310            }]
311        }
312        AgentMessage::ToolCallRequest {
313            id,
314            capability_token,
315            tool,
316            params,
317            ..
318        } if tool == "governed_transfer" => {
319            let metadata = serde_json::to_value(GovernedTransactionReceiptMetadata {
320                intent_id: "intent-governed-001".to_string(),
321                intent_hash: build_governed_intent()
322                    .binding_hash()
323                    .expect("hash deterministic governed intent"),
324                purpose: "settle supplier invoice".to_string(),
325                server_id: "conformance".to_string(),
326                tool_name: "governed_transfer".to_string(),
327                max_amount: None,
328                commerce: None,
329                metered_billing: None,
330                approval: None,
331                runtime_assurance: None,
332                call_chain: None,
333                autonomy: None,
334                economic_authorization: None,
335            })
336            .ok()
337            .map(|value| serde_json::json!({ "governed_transaction": value }));
338
339            vec![KernelMessage::ToolCallResponse {
340                id: id.clone(),
341                result: ToolCallResult::Ok {
342                    value: serde_json::json!({
343                        "ok": true,
344                        "tool": tool,
345                        "governed": true
346                    }),
347                },
348                receipt: Box::new(build_receipt(
349                    "rcpt-governed-001",
350                    &capability_token.id,
351                    tool,
352                    params.as_ref().clone(),
353                    Decision::Allow,
354                    metadata,
355                )),
356                execution_nonce: None,
357            }]
358        }
359        AgentMessage::ToolCallRequest {
360            id,
361            capability_token,
362            tool,
363            params,
364            ..
365        } => {
366            vec![KernelMessage::ToolCallResponse {
367                id: id.clone(),
368                result: ToolCallResult::Ok {
369                    value: serde_json::json!({
370                        "ok": true,
371                        "tool": tool,
372                        "fixture": "native"
373                    }),
374                },
375                receipt: Box::new(build_receipt(
376                    "rcpt-ok-001",
377                    &capability_token.id,
378                    tool,
379                    params.as_ref().clone(),
380                    Decision::Allow,
381                    None,
382                )),
383                execution_nonce: None,
384            }]
385        }
386    }
387}
388
389fn collect_native_scenarios(
390    path: &Path,
391    scenarios: &mut Vec<NativeScenarioDescriptor>,
392) -> Result<(), NativeSuiteError> {
393    for entry in fs::read_dir(path)? {
394        let entry = entry?;
395        let entry_path = entry.path();
396        let file_type = entry.file_type()?;
397        if file_type.is_symlink() {
398            return Err(NativeSuiteError::Io(std::io::Error::other(format!(
399                "refusing symlink in native conformance scenario tree: {}",
400                entry_path.display()
401            ))));
402        }
403        if file_type.is_dir() {
404            collect_native_scenarios(&entry_path, scenarios)?;
405        } else if file_type.is_file()
406            && entry_path.extension().and_then(|value| value.to_str()) == Some("json")
407        {
408            let content = fs::read_to_string(&entry_path)?;
409            let scenario =
410                serde_json::from_str(&content).map_err(|source| NativeSuiteError::Json {
411                    path: entry_path.display().to_string(),
412                    source,
413                })?;
414            scenarios.push(scenario);
415        }
416    }
417    Ok(())
418}
419
420fn require_native_scenario_directory(path: &Path) -> Result<(), NativeSuiteError> {
421    let metadata = fs::symlink_metadata(path).map_err(|source| {
422        NativeSuiteError::Io(std::io::Error::new(
423            source.kind(),
424            format!(
425                "native conformance scenario directory {} is not readable: {source}",
426                path.display()
427            ),
428        ))
429    })?;
430    let file_type = metadata.file_type();
431    if file_type.is_symlink() {
432        return Err(NativeSuiteError::Io(std::io::Error::other(format!(
433            "refusing symlinked native conformance scenario directory: {}",
434            path.display()
435        ))));
436    }
437    if !file_type.is_dir() {
438        return Err(NativeSuiteError::Io(std::io::Error::new(
439            std::io::ErrorKind::InvalidInput,
440            format!(
441                "native conformance scenario directory {} is not a directory",
442                path.display()
443            ),
444        )));
445    }
446    Ok(())
447}
448
449fn empty_native_scenario_directory_error(path: &Path) -> NativeSuiteError {
450    NativeSuiteError::Io(std::io::Error::new(
451        std::io::ErrorKind::InvalidData,
452        format!(
453            "native conformance scenario directory {} is empty: expected at least one JSON scenario",
454            path.display()
455        ),
456    ))
457}
458
459fn execute_native_scenario(
460    scenario: &NativeScenarioDescriptor,
461    options: &NativeConformanceRunOptions,
462) -> Result<NativeScenarioResult, NativeSuiteError> {
463    let start = Instant::now();
464    let outcome = match scenario.driver {
465        NativeDriver::Artifact => execute_artifact_scenario(scenario),
466        NativeDriver::Stdio => execute_stdio_scenario(scenario, options),
467        NativeDriver::Http => execute_http_scenario(scenario, options),
468    }?;
469
470    let duration_ms = start.elapsed().as_millis() as u64;
471    let status = if outcome
472        .assertions
473        .iter()
474        .all(|assertion| assertion.status == NativeStatus::Pass)
475    {
476        NativeStatus::Pass
477    } else {
478        NativeStatus::Fail
479    };
480    let failure_message = if status == NativeStatus::Fail {
481        outcome
482            .assertions
483            .iter()
484            .find(|assertion| assertion.status == NativeStatus::Fail)
485            .and_then(|assertion| assertion.message.clone())
486    } else {
487        None
488    };
489
490    Ok(NativeScenarioResult {
491        scenario_id: scenario.id.clone(),
492        title: scenario.title.clone(),
493        category: scenario.category,
494        driver: scenario.driver,
495        spec_version: scenario.spec_version.clone(),
496        status,
497        duration_ms,
498        assertions: outcome.assertions,
499        notes: scenario.notes.clone(),
500        failure_message,
501    })
502}
503
504struct ScenarioOutcome {
505    assertions: Vec<NativeAssertionResult>,
506}
507
508fn execute_artifact_scenario(
509    scenario: &NativeScenarioDescriptor,
510) -> Result<ScenarioOutcome, NativeSuiteError> {
511    let fixture = build_fixture(&scenario.fixture)?;
512    let assertions = scenario
513        .assertions
514        .iter()
515        .map(|assertion| evaluate_artifact_assertion(assertion, &fixture))
516        .collect::<Result<Vec<_>, _>>()?;
517    Ok(ScenarioOutcome { assertions })
518}
519
520fn execute_stdio_scenario(
521    scenario: &NativeScenarioDescriptor,
522    options: &NativeConformanceRunOptions,
523) -> Result<ScenarioOutcome, NativeSuiteError> {
524    let fixture = build_fixture(&scenario.fixture)?;
525    let request = fixture
526        .request()
527        .ok_or_else(|| NativeSuiteError::UnknownFixture(scenario.fixture.clone()))?;
528    let command =
529        options
530            .stdio_command
531            .as_ref()
532            .ok_or_else(|| NativeSuiteError::MissingStdioCommand {
533                scenario: scenario.id.clone(),
534            })?;
535
536    let mut child = Command::new(command)
537        .current_dir(&options.repo_root)
538        .stdin(Stdio::piped())
539        .stdout(Stdio::piped())
540        .stderr(Stdio::null())
541        .spawn()?;
542
543    let mut child_stdin = child
544        .stdin
545        .take()
546        .ok_or_else(|| NativeSuiteError::Io(std::io::Error::other("failed to open child stdin")))?;
547    let mut child_stdout = child.stdout.take().ok_or_else(|| {
548        NativeSuiteError::Io(std::io::Error::other("failed to open child stdout"))
549    })?;
550
551    let request_bytes = canonical_json_bytes(&request)
552        .map_err(|error| NativeSuiteError::Http(error.to_string()))?;
553    write_frame(&mut child_stdin, &request_bytes)?;
554    child_stdin.flush()?;
555    drop(child_stdin);
556
557    let messages = read_kernel_messages(&mut child_stdout)?;
558    let _ = child.wait();
559    let assertions = scenario
560        .assertions
561        .iter()
562        .map(|assertion| evaluate_message_assertion(assertion, &messages))
563        .collect::<Result<Vec<_>, _>>()?;
564
565    Ok(ScenarioOutcome { assertions })
566}
567
568fn execute_http_scenario(
569    scenario: &NativeScenarioDescriptor,
570    options: &NativeConformanceRunOptions,
571) -> Result<ScenarioOutcome, NativeSuiteError> {
572    let fixture = build_fixture(&scenario.fixture)?;
573    let request = fixture
574        .request()
575        .ok_or_else(|| NativeSuiteError::UnknownFixture(scenario.fixture.clone()))?;
576    let base_url =
577        options
578            .http_base_url
579            .as_ref()
580            .ok_or_else(|| NativeSuiteError::MissingHttpBaseUrl {
581                scenario: scenario.id.clone(),
582            })?;
583    let path = scenario
584        .http_path
585        .clone()
586        .unwrap_or_else(|| "/chio-conformance/v1/invoke".to_string());
587    let url = format!(
588        "{}{}",
589        base_url.trim_end_matches('/'),
590        if path.starts_with('/') {
591            path
592        } else {
593            format!("/{path}")
594        }
595    );
596    // CHIO_EGRESS_LINT_ALLOW_DIRECT_REQWEST: the native conformance HTTP
597    // driver targets a caller-supplied harness endpoint, not production
598    // substrate tool egress.
599    let client = Client::builder()
600        .timeout(Duration::from_secs(5))
601        .build()
602        .map_err(|error| NativeSuiteError::Http(error.to_string()))?;
603    let response = client
604        .post(&url)
605        .json(&NativeFixtureRequest {
606            scenario_id: scenario.id.clone(),
607            request,
608        })
609        // CHIO_EGRESS_LINT_ALLOW_DIRECT_REQWEST: native conformance harness
610        // dispatch, outside substrate tool egress.
611        .send()
612        .map_err(|error| NativeSuiteError::Http(error.to_string()))?;
613    if !response.status().is_success() {
614        return Err(NativeSuiteError::Http(format!(
615            "unexpected status {} from {url}",
616            response.status()
617        )));
618    }
619    let response: NativeFixtureResponse = response
620        .json()
621        .map_err(|error| NativeSuiteError::Http(error.to_string()))?;
622    let assertions = scenario
623        .assertions
624        .iter()
625        .map(|assertion| evaluate_message_assertion(assertion, &response.messages))
626        .collect::<Result<Vec<_>, _>>()?;
627
628    Ok(ScenarioOutcome { assertions })
629}
630
631fn read_kernel_messages(reader: &mut impl Read) -> Result<Vec<KernelMessage>, NativeSuiteError> {
632    let mut messages = Vec::new();
633    loop {
634        match read_frame(reader) {
635            Ok(frame) => {
636                let message: KernelMessage =
637                    serde_json::from_slice(&frame).map_err(|source| NativeSuiteError::Json {
638                        path: "<stdio>".to_string(),
639                        source,
640                    })?;
641                let terminal = matches!(message, KernelMessage::ToolCallResponse { .. });
642                messages.push(message);
643                if terminal {
644                    break;
645                }
646            }
647            Err(TransportError::ConnectionClosed) => break,
648            Err(error) => return Err(error.into()),
649        }
650    }
651    Ok(messages)
652}
653
654fn evaluate_artifact_assertion(
655    assertion: &NativeAssertionSpec,
656    fixture: &NativeFixture,
657) -> Result<NativeAssertionResult, NativeSuiteError> {
658    match assertion.kind {
659        NativeAssertionKind::CapabilitySignatureValid => {
660            let actual = fixture
661                .valid_capability()?
662                .verify_signature()
663                .unwrap_or(false);
664            compare_bool_assertion(assertion, actual)
665        }
666        NativeAssertionKind::DelegationChainValid => {
667            let (_, child) = fixture.delegation_pair()?;
668            let actual = validate_delegation_chain(&child.delegation_chain, Some(4)).is_ok();
669            compare_bool_assertion(assertion, actual)
670        }
671        NativeAssertionKind::DelegationAttenuatesParent => {
672            let (parent, child) = fixture.delegation_pair()?;
673            let actual = validate_attenuation(&parent.scope, &child.scope).is_ok();
674            compare_bool_assertion(assertion, actual)
675        }
676        NativeAssertionKind::ReceiptSignatureValid => {
677            let actual = fixture.valid_receipt()?.verify_signature().unwrap_or(false);
678            compare_bool_assertion(assertion, actual)
679        }
680        NativeAssertionKind::ReceiptTamperRejected => {
681            let actual = !fixture
682                .tampered_receipt()?
683                .verify_signature()
684                .unwrap_or(false);
685            compare_bool_assertion(assertion, actual)
686        }
687        NativeAssertionKind::DpopProofValid => {
688            let dpop = fixture.dpop_case()?;
689            let nonce_store = DpopNonceStore::new(32, Duration::from_secs(60));
690            let actual = verify_dpop_proof(
691                dpop.proof,
692                dpop.capability,
693                dpop.expected_tool_server,
694                dpop.expected_tool_name,
695                dpop.expected_action_hash,
696                &nonce_store,
697                &DpopConfig::default(),
698            )
699            .is_ok();
700            compare_bool_assertion(assertion, actual)
701        }
702        _ => Ok(NativeAssertionResult {
703            name: assertion.name.clone(),
704            status: NativeStatus::Fail,
705            message: Some("assertion kind requires message-driven execution".to_string()),
706        }),
707    }
708}
709
710fn evaluate_message_assertion(
711    assertion: &NativeAssertionSpec,
712    messages: &[KernelMessage],
713) -> Result<NativeAssertionResult, NativeSuiteError> {
714    match assertion.kind {
715        NativeAssertionKind::TerminalStatus => {
716            let (result, _) = terminal_response(messages).ok_or_else(|| {
717                NativeSuiteError::MissingTerminalResponse {
718                    scenario: assertion.name.clone(),
719                }
720            })?;
721            let actual = tool_result_status(result).to_string();
722            compare_string_assertion(assertion, actual)
723        }
724        NativeAssertionKind::ToolErrorCode => {
725            let (result, _) = terminal_response(messages).ok_or_else(|| {
726                NativeSuiteError::MissingTerminalResponse {
727                    scenario: assertion.name.clone(),
728                }
729            })?;
730            let actual = match result {
731                ToolCallResult::Err { error } => tool_error_code(error).to_string(),
732                _ => "not_an_error".to_string(),
733            };
734            compare_string_assertion(assertion, actual)
735        }
736        NativeAssertionKind::ResponseReceiptSignatureValid => {
737            let (_, receipt) = terminal_response(messages).ok_or_else(|| {
738                NativeSuiteError::MissingTerminalResponse {
739                    scenario: assertion.name.clone(),
740                }
741            })?;
742            let actual = receipt.verify_signature().unwrap_or(false);
743            compare_bool_assertion(assertion, actual)
744        }
745        NativeAssertionKind::GovernedReceiptPresent => {
746            let (_, receipt) = terminal_response(messages).ok_or_else(|| {
747                NativeSuiteError::MissingTerminalResponse {
748                    scenario: assertion.name.clone(),
749                }
750            })?;
751            let actual = receipt
752                .metadata
753                .as_ref()
754                .and_then(|value| value.get("governed_transaction"))
755                .is_some();
756            compare_bool_assertion(assertion, actual)
757        }
758        _ => Ok(NativeAssertionResult {
759            name: assertion.name.clone(),
760            status: NativeStatus::Fail,
761            message: Some("assertion kind requires artifact execution".to_string()),
762        }),
763    }
764}
765
766fn compare_bool_assertion(
767    assertion: &NativeAssertionSpec,
768    actual: bool,
769) -> Result<NativeAssertionResult, NativeSuiteError> {
770    let expected = assertion.expected_bool.ok_or_else(|| {
771        NativeSuiteError::Http(format!(
772            "assertion {} is missing expectedBool",
773            assertion.name
774        ))
775    })?;
776    Ok(NativeAssertionResult {
777        name: assertion.name.clone(),
778        status: if actual == expected {
779            NativeStatus::Pass
780        } else {
781            NativeStatus::Fail
782        },
783        message: if actual == expected {
784            None
785        } else {
786            Some(format!("expected {expected}, got {actual}"))
787        },
788    })
789}
790
791fn compare_string_assertion(
792    assertion: &NativeAssertionSpec,
793    actual: String,
794) -> Result<NativeAssertionResult, NativeSuiteError> {
795    let expected = assertion.expected_string.clone().ok_or_else(|| {
796        NativeSuiteError::Http(format!(
797            "assertion {} is missing expectedString",
798            assertion.name
799        ))
800    })?;
801    Ok(NativeAssertionResult {
802        name: assertion.name.clone(),
803        status: if actual == expected {
804            NativeStatus::Pass
805        } else {
806            NativeStatus::Fail
807        },
808        message: if actual == expected {
809            None
810        } else {
811            Some(format!("expected `{expected}`, got `{actual}`"))
812        },
813    })
814}
815
816fn terminal_response(messages: &[KernelMessage]) -> Option<(&ToolCallResult, &ChioReceipt)> {
817    messages.iter().find_map(|message| match message {
818        KernelMessage::ToolCallResponse {
819            result, receipt, ..
820        } => Some((result, receipt.as_ref())),
821        _ => None,
822    })
823}
824
825fn tool_result_status(result: &ToolCallResult) -> &'static str {
826    match result {
827        ToolCallResult::Ok { .. } => "ok",
828        ToolCallResult::StreamComplete { .. } => "stream_complete",
829        ToolCallResult::Cancelled { .. } => "cancelled",
830        ToolCallResult::Incomplete { .. } => "incomplete",
831        ToolCallResult::Err { .. } => "err",
832    }
833}
834
835fn tool_error_code(error: &ToolCallError) -> &'static str {
836    match error {
837        ToolCallError::CapabilityDenied(_) => "capability_denied",
838        ToolCallError::CapabilityExpired => "capability_expired",
839        ToolCallError::CapabilityRevoked => "capability_revoked",
840        ToolCallError::PolicyDenied { .. } => "policy_denied",
841        ToolCallError::ToolServerError(_) => "tool_server_error",
842        ToolCallError::InternalError(_) => "internal_error",
843    }
844}
845
846fn generate_native_markdown_report(results: &[NativeScenarioResult]) -> String {
847    let mut output = String::new();
848    output.push_str("# Chio Native Conformance Report\n\n");
849    output.push_str("Generated from native conformance result artifacts.\n\n");
850
851    if results.is_empty() {
852        output.push_str("No native conformance results were generated.\n");
853        return output;
854    }
855
856    output.push_str("## Summary\n\n");
857    for category in [
858        NativeScenarioCategory::CapabilityValidation,
859        NativeScenarioCategory::DelegationAttenuation,
860        NativeScenarioCategory::ReceiptIntegrity,
861        NativeScenarioCategory::RevocationPropagation,
862        NativeScenarioCategory::DpopVerification,
863        NativeScenarioCategory::GovernedTransactionEnforcement,
864    ] {
865        let category_results = results
866            .iter()
867            .filter(|result| result.category == category)
868            .collect::<Vec<_>>();
869        if category_results.is_empty() {
870            continue;
871        }
872        let passed = category_results
873            .iter()
874            .filter(|result| result.status == NativeStatus::Pass)
875            .count();
876        output.push_str(&format!(
877            "- {}: {passed}/{} pass\n",
878            category.heading(),
879            category_results.len()
880        ));
881    }
882    output.push('\n');
883
884    for category in [
885        NativeScenarioCategory::CapabilityValidation,
886        NativeScenarioCategory::DelegationAttenuation,
887        NativeScenarioCategory::ReceiptIntegrity,
888        NativeScenarioCategory::RevocationPropagation,
889        NativeScenarioCategory::DpopVerification,
890        NativeScenarioCategory::GovernedTransactionEnforcement,
891    ] {
892        let category_results = results
893            .iter()
894            .filter(|result| result.category == category)
895            .collect::<Vec<_>>();
896        if category_results.is_empty() {
897            continue;
898        }
899        output.push_str(&format!("## {}\n\n", category.heading()));
900        output.push_str("| Scenario | Driver | Status | Duration |\n");
901        output.push_str("| --- | --- | --- | --- |\n");
902        for result in category_results {
903            output.push_str(&format!(
904                "| `{}` | `{}` | `{}` | {} ms |\n",
905                result.scenario_id,
906                result.driver.label(),
907                result.status.label(),
908                result.duration_ms
909            ));
910        }
911        output.push('\n');
912    }
913
914    let failures = results
915        .iter()
916        .filter(|result| result.status == NativeStatus::Fail)
917        .collect::<Vec<_>>();
918    if !failures.is_empty() {
919        output.push_str("## Failures\n\n");
920        for failure in failures {
921            output.push_str(&format!(
922                "- `{}`: {}\n",
923                failure.scenario_id,
924                failure
925                    .failure_message
926                    .as_deref()
927                    .unwrap_or("scenario failed without a recorded failure message")
928            ));
929        }
930    }
931
932    output
933}
934
935enum NativeFixture {
936    Capability(Box<CapabilityToken>),
937    Delegation {
938        parent: Box<CapabilityToken>,
939        child: Box<CapabilityToken>,
940    },
941    Receipt {
942        valid: Box<ChioReceipt>,
943        tampered: Box<ChioReceipt>,
944    },
945    Dpop {
946        proof: Box<DpopProof>,
947        capability: Box<CapabilityToken>,
948        expected_tool_server: String,
949        expected_tool_name: String,
950        expected_action_hash: String,
951    },
952    Request(AgentMessage),
953}
954
955impl NativeFixture {
956    fn valid_capability(&self) -> Result<&CapabilityToken, NativeSuiteError> {
957        match self {
958            Self::Capability(token) => Ok(token),
959            _ => Err(NativeSuiteError::Http(
960                "fixture is not a capability".to_string(),
961            )),
962        }
963    }
964
965    fn delegation_pair(&self) -> Result<(&CapabilityToken, &CapabilityToken), NativeSuiteError> {
966        match self {
967            Self::Delegation { parent, child } => Ok((parent.as_ref(), child.as_ref())),
968            _ => Err(NativeSuiteError::Http(
969                "fixture is not a delegation pair".to_string(),
970            )),
971        }
972    }
973
974    fn valid_receipt(&self) -> Result<&ChioReceipt, NativeSuiteError> {
975        match self {
976            Self::Receipt { valid, .. } => Ok(valid.as_ref()),
977            _ => Err(NativeSuiteError::Http(
978                "fixture is not a receipt".to_string(),
979            )),
980        }
981    }
982
983    fn tampered_receipt(&self) -> Result<&ChioReceipt, NativeSuiteError> {
984        match self {
985            Self::Receipt { tampered, .. } => Ok(tampered.as_ref()),
986            _ => Err(NativeSuiteError::Http(
987                "fixture is not a receipt".to_string(),
988            )),
989        }
990    }
991
992    fn dpop_case(&self) -> Result<DpopCase<'_>, NativeSuiteError> {
993        match self {
994            Self::Dpop {
995                proof,
996                capability,
997                expected_tool_server,
998                expected_tool_name,
999                expected_action_hash,
1000            } => Ok(DpopCase {
1001                proof: proof.as_ref(),
1002                capability,
1003                expected_tool_server,
1004                expected_tool_name,
1005                expected_action_hash,
1006            }),
1007            _ => Err(NativeSuiteError::Http(
1008                "fixture is not a dpop case".to_string(),
1009            )),
1010        }
1011    }
1012
1013    fn request(&self) -> Option<AgentMessage> {
1014        match self {
1015            Self::Request(request) => Some(request.clone()),
1016            _ => None,
1017        }
1018    }
1019}
1020
1021struct DpopCase<'a> {
1022    proof: &'a DpopProof,
1023    capability: &'a CapabilityToken,
1024    expected_tool_server: &'a str,
1025    expected_tool_name: &'a str,
1026    expected_action_hash: &'a str,
1027}
1028
1029fn build_fixture(id: &str) -> Result<NativeFixture, NativeSuiteError> {
1030    match id {
1031        "valid_capability" => Ok(NativeFixture::Capability(
1032            Box::new(build_valid_capability()),
1033        )),
1034        "delegation_pair" => {
1035            let (parent, child) = build_delegation_pair();
1036            Ok(NativeFixture::Delegation {
1037                parent: Box::new(parent),
1038                child: Box::new(child),
1039            })
1040        }
1041        "signed_receipt" => {
1042            let valid = build_receipt(
1043                "rcpt-integrity-001",
1044                "cap-valid-001",
1045                "echo",
1046                serde_json::json!({"text": "hello"}),
1047                Decision::Allow,
1048                None,
1049            );
1050            let mut tampered = valid.clone();
1051            tampered.tool_name = "tampered".to_string();
1052            Ok(NativeFixture::Receipt {
1053                valid: Box::new(valid),
1054                tampered: Box::new(tampered),
1055            })
1056        }
1057        "valid_dpop" => {
1058            let capability = build_dpop_capability();
1059            let params = serde_json::json!({"amount": 25, "currency": "USD"});
1060            let action_hash = chio_core::sha256_hex(
1061                &canonical_json_bytes(&params)
1062                    .map_err(|error| NativeSuiteError::Http(error.to_string()))?,
1063            );
1064            let proof = DpopProof::sign(
1065                DpopProofBody {
1066                    schema: chio_kernel::dpop::DPOP_SCHEMA.to_string(),
1067                    capability_id: capability.id.clone(),
1068                    tool_server: "conformance".to_string(),
1069                    tool_name: "transfer".to_string(),
1070                    action_hash: action_hash.clone(),
1071                    nonce: "nonce-001".to_string(),
1072                    issued_at: current_unix_timestamp(),
1073                    agent_key: dpop_subject_keypair().public_key(),
1074                },
1075                &dpop_subject_keypair(),
1076            )
1077            .map_err(|error| NativeSuiteError::Http(error.to_string()))?;
1078            Ok(NativeFixture::Dpop {
1079                proof: Box::new(proof),
1080                capability: Box::new(capability),
1081                expected_tool_server: "conformance".to_string(),
1082                expected_tool_name: "transfer".to_string(),
1083                expected_action_hash: action_hash,
1084            })
1085        }
1086        "revoked_capability_request" => Ok(NativeFixture::Request(build_revoked_request())),
1087        "governed_request" => Ok(NativeFixture::Request(build_governed_request())),
1088        other => Err(NativeSuiteError::UnknownFixture(other.to_string())),
1089    }
1090}
1091
1092fn authority_keypair() -> Keypair {
1093    Keypair::from_seed(&[7u8; 32])
1094}
1095
1096fn capability_subject_keypair() -> Keypair {
1097    Keypair::from_seed(&[11u8; 32])
1098}
1099
1100fn delegated_subject_keypair() -> Keypair {
1101    Keypair::from_seed(&[13u8; 32])
1102}
1103
1104fn dpop_subject_keypair() -> Keypair {
1105    Keypair::from_seed(&[17u8; 32])
1106}
1107
1108fn kernel_keypair() -> Keypair {
1109    Keypair::from_seed(&[23u8; 32])
1110}
1111
1112fn build_scope(
1113    tool_name: &str,
1114    dpop_required: Option<bool>,
1115    constraints: Vec<Constraint>,
1116) -> ChioScope {
1117    ChioScope {
1118        grants: vec![ToolGrant {
1119            server_id: "conformance".to_string(),
1120            tool_name: tool_name.to_string(),
1121            operations: vec![Operation::Invoke],
1122            constraints,
1123            max_invocations: Some(5),
1124            max_cost_per_invocation: None,
1125            max_total_cost: None,
1126            dpop_required,
1127        }],
1128        ..ChioScope::default()
1129    }
1130}
1131
1132#[allow(clippy::expect_used)]
1133fn build_capability(
1134    id: &str,
1135    subject: &Keypair,
1136    scope: ChioScope,
1137    delegation_chain: Vec<DelegationLink>,
1138) -> CapabilityToken {
1139    CapabilityToken::sign(
1140        CapabilityTokenBody {
1141            id: id.to_string(),
1142            issuer: authority_keypair().public_key(),
1143            subject: subject.public_key(),
1144            scope,
1145            issued_at: 1_700_000_000,
1146            expires_at: 1_800_000_000,
1147            delegation_chain,
1148            aggregate_invocation_budget: None,
1149        },
1150        &authority_keypair(),
1151    )
1152    .expect("sign deterministic capability")
1153}
1154
1155fn build_valid_capability() -> CapabilityToken {
1156    build_capability(
1157        "cap-valid-001",
1158        &capability_subject_keypair(),
1159        build_scope("echo", None, vec![]),
1160        vec![],
1161    )
1162}
1163
1164fn build_dpop_capability() -> CapabilityToken {
1165    build_capability(
1166        "cap-dpop-001",
1167        &dpop_subject_keypair(),
1168        build_scope("transfer", Some(true), vec![]),
1169        vec![],
1170    )
1171}
1172
1173#[allow(clippy::expect_used)]
1174fn build_delegation_pair() -> (CapabilityToken, CapabilityToken) {
1175    let parent_subject = capability_subject_keypair();
1176    let child_subject = delegated_subject_keypair();
1177    let parent = build_capability(
1178        "cap-parent-001",
1179        &parent_subject,
1180        build_scope("echo", None, vec![]),
1181        vec![],
1182    );
1183    let child_scope = ChioScope {
1184        grants: vec![ToolGrant {
1185            server_id: "conformance".to_string(),
1186            tool_name: "echo".to_string(),
1187            operations: vec![Operation::Invoke],
1188            constraints: vec![Constraint::MaxLength(32)],
1189            max_invocations: Some(1),
1190            max_cost_per_invocation: None,
1191            max_total_cost: None,
1192            dpop_required: None,
1193        }],
1194        ..ChioScope::default()
1195    };
1196    let child_scope_hash = scope_hash(&child_scope).expect("hash child delegation scope");
1197    let delegation = DelegationLink::sign(
1198        DelegationLinkBody {
1199            capability_id: parent.id.clone(),
1200            delegator: parent_subject.public_key(),
1201            delegatee: child_subject.public_key(),
1202            attenuations: vec![
1203                Attenuation::ReduceBudget {
1204                    server_id: "conformance".to_string(),
1205                    tool_name: "echo".to_string(),
1206                    max_invocations: 1,
1207                },
1208                Attenuation::AddConstraint {
1209                    server_id: "conformance".to_string(),
1210                    tool_name: "echo".to_string(),
1211                    constraint: Constraint::MaxLength(32),
1212                },
1213            ],
1214            timestamp: 1_700_000_100,
1215            scope_hash: Some(child_scope_hash),
1216            aggregate_budget: None,
1217            cumulative_approval: None,
1218        },
1219        &parent_subject,
1220    )
1221    .expect("sign deterministic delegation");
1222
1223    let child = build_capability(
1224        "cap-child-001",
1225        &child_subject,
1226        child_scope,
1227        vec![delegation],
1228    );
1229    (parent, child)
1230}
1231
1232fn build_governed_intent() -> GovernedTransactionIntent {
1233    GovernedTransactionIntent {
1234        id: "intent-governed-001".to_string(),
1235        server_id: "conformance".to_string(),
1236        tool_name: "governed_transfer".to_string(),
1237        purpose: "settle supplier invoice".to_string(),
1238        max_amount: None,
1239        commerce: None,
1240        metered_billing: None,
1241        runtime_attestation: None,
1242        call_chain: None,
1243        autonomy: None,
1244        context: Some(serde_json::json!({
1245            "amount": 1250,
1246            "currency": "USD",
1247            "seller": "supplier-001"
1248        })),
1249        body: Default::default(),
1250    }
1251}
1252
1253fn build_governed_request() -> AgentMessage {
1254    AgentMessage::ToolCallRequest {
1255        id: "req-governed-001".to_string(),
1256        capability_token: Box::new(build_capability(
1257            "cap-governed-001",
1258            &capability_subject_keypair(),
1259            build_scope(
1260                "governed_transfer",
1261                None,
1262                vec![Constraint::GovernedIntentRequired],
1263            ),
1264            vec![],
1265        )),
1266        server_id: "conformance".to_string(),
1267        tool: "governed_transfer".to_string(),
1268        params: Box::new(serde_json::json!({
1269            "amount": 1250,
1270            "currency": "USD",
1271            "seller": "supplier-001"
1272        })),
1273        governed_intent: None,
1274        approval_token: None,
1275        approval_tokens: Vec::new(),
1276        threshold_approval_proposal: None,
1277        supplemental_authorization: None,
1278        execution_nonce: None,
1279    }
1280}
1281
1282fn build_revoked_request() -> AgentMessage {
1283    AgentMessage::ToolCallRequest {
1284        id: "req-revoked-001".to_string(),
1285        capability_token: Box::new(build_capability(
1286            "cap-revoked-001",
1287            &capability_subject_keypair(),
1288            build_scope("echo", None, vec![]),
1289            vec![],
1290        )),
1291        server_id: "conformance".to_string(),
1292        tool: "echo".to_string(),
1293        params: Box::new(serde_json::json!({"text": "hello"})),
1294        governed_intent: None,
1295        approval_token: None,
1296        approval_tokens: Vec::new(),
1297        threshold_approval_proposal: None,
1298        supplemental_authorization: None,
1299        execution_nonce: None,
1300    }
1301}
1302
1303#[allow(clippy::expect_used)]
1304fn build_receipt(
1305    receipt_id: &str,
1306    capability_id: &str,
1307    tool_name: &str,
1308    params: serde_json::Value,
1309    decision: Decision,
1310    metadata: Option<serde_json::Value>,
1311) -> ChioReceipt {
1312    ChioReceipt::sign(
1313        ChioReceiptBody {
1314            id: receipt_id.to_string(),
1315            timestamp: 1_700_000_200,
1316            capability_id: capability_id.to_string(),
1317            tool_server: "conformance".to_string(),
1318            tool_name: tool_name.to_string(),
1319            action: ToolCallAction::from_parameters(params).expect("build action"),
1320            decision: Some(decision),
1321            receipt_kind: ReceiptKind::MediatedDecision,
1322            boundary_class: BoundaryClass::Prevent,
1323            observation_outcome: None,
1324            tool_origin: ToolOrigin::CallerExecuted,
1325            redaction_mode: RedactionMode::None,
1326            actor_chain: Vec::new(),
1327            content_hash: chio_core::sha256_hex(b"{\"ok\":true}"),
1328            policy_hash: "policy-hash-001".to_string(),
1329            evidence: vec![GuardEvidence {
1330                guard_name: "ConformanceGuard".to_string(),
1331                verdict: true,
1332                details: None,
1333            }],
1334            metadata,
1335            trust_level: chio_core::receipt::kinds::TrustLevel::default(),
1336            tenant_id: None,
1337            kernel_key: kernel_keypair().public_key(),
1338            bbs_projection_version: None,
1339        },
1340        &kernel_keypair(),
1341    )
1342    .expect("sign deterministic receipt")
1343}
1344
1345fn current_unix_timestamp() -> u64 {
1346    SystemTime::now()
1347        .duration_since(UNIX_EPOCH)
1348        .map(|duration| duration.as_secs())
1349        .unwrap_or(0)
1350}
1351
1352#[cfg(test)]
1353#[allow(clippy::expect_used, clippy::unwrap_used)]
1354mod tests {
1355    use super::*;
1356
1357    #[test]
1358    fn load_native_scenarios_reads_checked_in_suite() {
1359        let repo_root = crate::default_repo_root();
1360        let scenarios =
1361            load_native_scenarios_from_dir(repo_root.join("tests/conformance/native/scenarios"))
1362                .expect("load native scenarios");
1363        assert_eq!(scenarios.len(), 6);
1364        assert!(scenarios
1365            .iter()
1366            .any(|scenario| { scenario.category == NativeScenarioCategory::CapabilityValidation }));
1367        assert!(scenarios.iter().any(|scenario| {
1368            scenario.category == NativeScenarioCategory::GovernedTransactionEnforcement
1369        }));
1370    }
1371
1372    #[test]
1373    fn load_native_scenarios_rejects_missing_directory() {
1374        let missing = std::env::temp_dir().join(format!(
1375            "chio-conformance-native-missing-{}",
1376            std::process::id()
1377        ));
1378        let _ = fs::remove_dir_all(&missing);
1379
1380        match load_native_scenarios_from_dir(&missing) {
1381            Ok(scenarios) => panic!("missing native scenario directory should fail: {scenarios:?}"),
1382            Err(error) => assert!(error.to_string().contains("directory")),
1383        }
1384    }
1385
1386    #[test]
1387    fn load_native_scenarios_rejects_empty_directory() {
1388        let dir = std::env::temp_dir().join(format!(
1389            "chio-conformance-native-empty-{}",
1390            std::process::id()
1391        ));
1392        let _ = fs::remove_dir_all(&dir);
1393        fs::create_dir_all(&dir).expect("create empty native scenario dir");
1394
1395        match load_native_scenarios_from_dir(&dir) {
1396            Ok(scenarios) => panic!("empty native scenario directory should fail: {scenarios:?}"),
1397            Err(error) => assert!(error.to_string().contains("empty")),
1398        }
1399
1400        let _ = fs::remove_dir_all(dir);
1401    }
1402
1403    #[cfg(unix)]
1404    #[test]
1405    fn load_native_scenarios_rejects_symlinked_json_file() {
1406        let dir = std::env::temp_dir().join(format!(
1407            "chio-conformance-native-symlink-{}",
1408            std::process::id()
1409        ));
1410        let outside = std::env::temp_dir().join(format!(
1411            "chio-conformance-native-symlink-outside-{}",
1412            std::process::id()
1413        ));
1414        let _ = fs::remove_dir_all(&dir);
1415        let _ = fs::remove_dir_all(&outside);
1416        fs::create_dir_all(&dir).expect("create native scenario dir");
1417        fs::create_dir_all(&outside).expect("create outside dir");
1418        fs::write(
1419            outside.join("escape.json"),
1420            r#"{
1421              "id": "escape",
1422              "title": "Escape",
1423              "category": "capability_validation",
1424              "driver": "artifact",
1425              "fixture": "valid-capability",
1426              "specVersion": "1.0",
1427              "assertions": []
1428            }"#,
1429        )
1430        .expect("write outside scenario");
1431        std::os::unix::fs::symlink(outside.join("escape.json"), dir.join("escape.json"))
1432            .expect("create scenario symlink");
1433
1434        match load_native_scenarios_from_dir(&dir) {
1435            Ok(scenarios) => panic!("symlinked native scenario should fail: {scenarios:?}"),
1436            Err(error) => assert!(error.to_string().contains("symlink")),
1437        }
1438
1439        let _ = fs::remove_dir_all(dir);
1440        let _ = fs::remove_dir_all(outside);
1441    }
1442
1443    #[test]
1444    fn native_fixture_responses_include_governed_receipt_metadata() {
1445        let request = build_governed_request();
1446        let messages = fixture_messages_for_request(&request);
1447        let (_, receipt) = terminal_response(&messages).expect("terminal response");
1448        assert!(receipt.verify_signature().expect("verify signature"));
1449        assert!(receipt
1450            .metadata
1451            .as_ref()
1452            .and_then(|value| value.get("governed_transaction"))
1453            .is_some());
1454    }
1455}