use serde::{Deserialize, Serialize};
pub const CAPABILITY_COVERAGE_SCHEMA_VERSION: u32 = 1;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CapabilityCoverageScenario {
NativeSuccess,
ExternalSuccess,
PolicyRejection,
ExternalTimeout,
ExternalDisabled,
}
impl CapabilityCoverageScenario {
const REQUIRED: [Self; 5] = [
Self::NativeSuccess,
Self::ExternalSuccess,
Self::PolicyRejection,
Self::ExternalTimeout,
Self::ExternalDisabled,
];
const fn expected_result(self) -> CapabilityCoverageResult {
match self {
Self::NativeSuccess | Self::ExternalSuccess => CapabilityCoverageResult::Succeeded,
Self::PolicyRejection => CapabilityCoverageResult::RejectedByPolicy,
Self::ExternalTimeout => CapabilityCoverageResult::TimedOut,
Self::ExternalDisabled => CapabilityCoverageResult::Disabled,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CapabilityCoverageResult {
Succeeded,
RejectedByPolicy,
TimedOut,
Disabled,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CapabilityCoverageCaseV1 {
pub scenario: CapabilityCoverageScenario,
pub capability_id: String,
pub capability_version: String,
pub observed: CapabilityCoverageResult,
}
impl CapabilityCoverageCaseV1 {
#[must_use]
pub fn new(
scenario: CapabilityCoverageScenario,
capability_id: impl Into<String>,
capability_version: impl Into<String>,
observed: CapabilityCoverageResult,
) -> Self {
Self {
scenario,
capability_id: capability_id.into(),
capability_version: capability_version.into(),
observed,
}
}
#[must_use]
pub fn passed(&self) -> bool {
self.scenario.expected_result() == self.observed
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CapabilityCoverageReportV1 {
pub schema_version: u32,
pub cases: Vec<CapabilityCoverageCaseV1>,
}
impl CapabilityCoverageReportV1 {
#[must_use]
pub fn new(cases: Vec<CapabilityCoverageCaseV1>) -> Self {
Self {
schema_version: CAPABILITY_COVERAGE_SCHEMA_VERSION,
cases,
}
}
#[must_use]
pub fn is_complete(&self) -> bool {
self.schema_version == CAPABILITY_COVERAGE_SCHEMA_VERSION
&& self.cases.len() == CapabilityCoverageScenario::REQUIRED.len()
&& self
.cases
.iter()
.zip(CapabilityCoverageScenario::REQUIRED)
.all(|(case, required)| case.scenario == required && case.passed())
}
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string(self)
}
}
#[cfg(test)]
mod report_tests {
use super::*;
#[test]
fn canonical_report_requires_each_passing_scenario_once() {
let cases = CapabilityCoverageScenario::REQUIRED.map(|scenario| {
CapabilityCoverageCaseV1::new(
scenario,
"capability://example/test",
"1.0.0",
scenario.expected_result(),
)
});
let report = CapabilityCoverageReportV1::new(cases.into());
assert!(report.is_complete());
assert_eq!(
report.to_json().expect("serializable report"),
report.to_json().expect("deterministic report")
);
let incomplete = CapabilityCoverageReportV1::new(Vec::new());
assert!(!incomplete.is_complete());
}
}
#[cfg(all(test, unix))]
mod tests {
use std::ffi::OsString;
use std::fs;
use std::sync::Arc;
use super::*;
use crate::core::ocla::adapters::external_process::ExternalProcessAdapter;
use crate::core::ocla::adapters::{AdapterRegistry, NativeContextAdapter};
use crate::core::ocla::invocation::{
CapabilityAdapter, CapabilityInput, CapabilityInvocation, PolicyConstraints,
};
const EXTERNAL_MANIFEST: &str = include_str!(
"../../../../../docs/contracts/ocla/capability-manifests/example/word-count-optimizer-v1.json"
);
fn native_invocation(adapter: &NativeContextAdapter) -> CapabilityInvocation {
CapabilityInvocation {
task_id: "capability-coverage/native-success".into(),
capability_id: adapter.manifest().capability_id.as_str().into(),
capability_version: adapter.manifest().version.clone(),
input: CapabilityInput::ContextRequest {
paths: vec!["fixture.rs".into()],
mode: "aggressive".into(),
budget_tokens: Some(100),
},
policy_constraints: PolicyConstraints::default(),
timeout_ms: 1_000,
}
}
fn external_invocation(timeout_ms: u64) -> CapabilityInvocation {
CapabilityInvocation {
task_id: "capability-coverage/external-success".into(),
capability_id: "capability://example/word-count-optimizer".into(),
capability_version: "1.0.0".into(),
input: CapabilityInput::ShellCommand {
command: "hello world foo".into(),
workdir: None,
},
policy_constraints: PolicyConstraints::default(),
timeout_ms,
}
}
fn write_manifest(temp: &tempfile::TempDir) -> std::path::PathBuf {
let path = temp.path().join("word-count-manifest.json");
fs::write(&path, EXTERNAL_MANIFEST).expect("external manifest fixture");
path
}
fn stdin_consuming_adapter(temp: &tempfile::TempDir) -> ExternalProcessAdapter {
ExternalProcessAdapter::discover(
write_manifest(temp),
"/bin/sh",
[
OsString::from("-c"),
OsString::from(
"while IFS= read -r _; do :; done; \
printf '%s' '{\"word_count\":3,\"char_count\":15,\"line_count\":1}'",
),
],
)
.expect("bounded external adapter")
}
#[test]
fn coverage_matrix_proves_registry_native_and_external_safety_behavior() {
let temp = tempfile::tempdir().expect("fixture directory");
fs::write(
temp.path().join("fixture.rs"),
"// fixture documentation\nfn main() { let value = 1; }\n",
)
.expect("native fixture");
let native = Arc::new(NativeContextAdapter::with_root(temp.path()));
let external = Arc::new(stdin_consuming_adapter(&temp));
let registry = AdapterRegistry::new();
registry
.register_arc(native.clone())
.expect("native registration");
registry
.register_arc(external.clone())
.expect("external registration");
let native_id = native.manifest().capability_id.as_str().to_owned();
let native_version = native.manifest().version.clone();
let external_id = external.manifest().capability_id.as_str().to_owned();
let external_version = external.manifest().version.clone();
let registered_native = registry
.lookup(&native_id, &native_version)
.expect("registered native adapter");
let registered_external = registry
.lookup(&external_id, &external_version)
.expect("registered external adapter");
assert!(registered_native.invoke(native_invocation(&native)).is_ok());
assert!(
registered_external
.invoke(external_invocation(1_000))
.is_ok()
);
let mut rejected_invocation = native_invocation(&native);
rejected_invocation.policy_constraints.allowed_paths = vec!["other.rs".into()];
let rejected = registered_native
.invoke(rejected_invocation)
.expect_err("native adapter must enforce path policy");
assert!(rejected.to_string().contains("outside policy allowlist"));
let timeout_adapter = ExternalProcessAdapter::discover(
write_manifest(&temp),
"/bin/sleep",
[OsString::from("1")],
)
.expect("bounded timeout adapter");
let timed_out = timeout_adapter
.invoke(external_invocation(1))
.expect_err("external process must honor timeout");
assert!(timed_out.to_string().contains("exceeded its timeout"));
external.disable();
let disabled = registered_external
.invoke(external_invocation(1_000))
.expect_err("disabled adapter must remain unavailable through registry");
assert!(disabled.to_string().contains("is disabled"));
assert_eq!(registry.len(), 2, "disable must not unregister the adapter");
assert_eq!(
registry
.health_check_all()
.expect("registry health")
.into_iter()
.filter(|health| !health.healthy)
.count(),
1,
"only the disabled external adapter is unhealthy"
);
let report = CapabilityCoverageReportV1::new(vec![
CapabilityCoverageCaseV1::new(
CapabilityCoverageScenario::NativeSuccess,
native_id.clone(),
native_version.clone(),
CapabilityCoverageResult::Succeeded,
),
CapabilityCoverageCaseV1::new(
CapabilityCoverageScenario::ExternalSuccess,
external_id.clone(),
external_version.clone(),
CapabilityCoverageResult::Succeeded,
),
CapabilityCoverageCaseV1::new(
CapabilityCoverageScenario::PolicyRejection,
native_id,
native_version,
CapabilityCoverageResult::RejectedByPolicy,
),
CapabilityCoverageCaseV1::new(
CapabilityCoverageScenario::ExternalTimeout,
external_id.clone(),
external_version.clone(),
CapabilityCoverageResult::TimedOut,
),
CapabilityCoverageCaseV1::new(
CapabilityCoverageScenario::ExternalDisabled,
external_id,
external_version,
CapabilityCoverageResult::Disabled,
),
]);
assert!(report.is_complete());
let json = report.to_json().expect("serializable coverage evidence");
assert_eq!(json, report.to_json().expect("deterministic report"));
assert_eq!(
serde_json::from_str::<CapabilityCoverageReportV1>(&json)
.expect("machine-readable report"),
report
);
}
}