Skip to main content

luft_core/contract/
mod.rs

1//! Frozen contracts (code-design ยง1). These types are the shared basis for all
2//! modules; once reviewed they should change rarely. A companion `CONTRACTS.md`
3//! may track the freeze.
4
5pub mod backend;
6pub mod cache;
7pub mod event;
8pub mod finding;
9pub mod ids;
10pub mod schema;
11pub mod skill;
12
13// backend: AgentBackend (trait), AgentTask, AgentResult, RunContext, BackendError
14pub use backend::*;
15// cache: agent_cache_key (single named export; no wildcard re-export)
16pub use cache::agent_cache_key;
17// event: AgentEvent, EventSender, RunStatus
18pub use event::*;
19// finding: Finding, Severity, Location
20pub use finding::*;
21// ids: RunId, AgentId, PhaseId, TokenUsage
22pub use ids::*;
23// schema: validate_output, SchemaError
24pub use schema::*;
25// skill: Skill
26pub use skill::Skill;
27
28#[cfg(test)]
29mod tests {
30    //! The `contract` module is the frozen public API surface of `luft-core`.
31    //! These tests are *compile-time* checks: they construct every re-exported
32    //! item so any future drift (e.g. accidentally removing a wildcard export,
33    //! renaming a type without updating this module) breaks the build before
34    //! it reaches downstream crates.
35
36    use super::*;
37
38    #[test]
39    fn reexports_are_accessible() {
40        // backend
41        let _: AgentCapabilities = AgentCapabilities::default();
42        let _: AgentStatus = AgentStatus::Ok;
43        let _: AgentTask;
44        let _: AgentResult;
45        let _: RunContext;
46        let _: BackendError = BackendError::Cancelled;
47        let _: ToolPolicy = ToolPolicy::default();
48        let _: McpEndpoint;
49        let _: Artifact;
50        let _: LogRef = LogRef::default();
51
52        // cache
53        let _: fn(&str, Option<&str>, &str, u32) -> String = agent_cache_key;
54
55        // event
56        let _: EventSender;
57        let _: AgentEvent;
58        let _: RunStatus = RunStatus::Completed;
59        let _: LogLevel = LogLevel::Info;
60        let _: PlanPhase;
61        let _: ProgressDelta;
62
63        // finding
64        let _: Finding;
65        let _: Severity = Severity::Info;
66        let _: Location;
67
68        // ids
69        let _: RunId = uuid::Uuid::nil();
70        let _: AgentId = uuid::Uuid::nil();
71        let _: PhaseId = 0u32;
72        let _: TokenUsage = TokenUsage::default();
73
74        // schema
75        let _: fn(&serde_json::Value, &serde_json::Value) -> Result<(), SchemaError> =
76            validate_output;
77
78        // skill
79        let _: Skill = Skill {
80            name: "n",
81            description: "d",
82            content: "c",
83            references: &[],
84        };
85    }
86
87    #[test]
88    fn submodule_paths_resolve() {
89        // Direct submodule paths must still resolve even though we also
90        // re-export with wildcards. This catches "moved submodule" breakage.
91        fn _assert_trait(_: &dyn backend::AgentBackend) {}
92        let _: event::AgentEvent;
93        let _: finding::Finding;
94        let _: ids::TokenUsage;
95        let _: schema::SchemaError;
96        // cache module exports the `agent_cache_key` function via its own path.
97        let _: fn(&str, Option<&str>, &str, u32) -> String = cache::agent_cache_key;
98        let _: skill::Skill;
99    }
100
101    #[test]
102    fn backend_error_is_retryable_helper() {
103        assert!(BackendError::Timeout.is_retryable());
104        assert!(BackendError::Spawn("x".into()).is_retryable());
105        assert!(!BackendError::Cancelled.is_retryable());
106        assert!(!BackendError::Protocol("x".into()).is_retryable());
107        assert!(!BackendError::Config("x".into()).is_retryable());
108    }
109
110    #[test]
111    fn agent_status_as_str_is_consistent() {
112        // Spot-check that re-exported `AgentStatus` retains its `as_str()`
113        // helper exposed by the backend submodule.
114        assert_eq!(AgentStatus::Ok.as_str(), "ok");
115        assert_eq!(AgentStatus::TimedOut.as_str(), "timed_out");
116    }
117
118    #[test]
119    fn token_usage_default_is_zero() {
120        let t = TokenUsage::default();
121        assert_eq!(t.total(), 0);
122    }
123}