use crate::{DrivenError, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SteeringRule {
pub id: String,
pub name: String,
pub inclusion: SteeringInclusion,
pub content: String,
pub file_references: Vec<FileReference>,
pub priority: u8,
pub source_path: Option<PathBuf>,
}
impl SteeringRule {
pub fn new(id: impl Into<String>) -> Self {
let id = id.into();
Self {
name: id.clone(),
id,
inclusion: SteeringInclusion::Always,
content: String::new(),
file_references: Vec::new(),
priority: 100,
source_path: None,
}
}
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = name.into();
self
}
pub fn with_inclusion(mut self, inclusion: SteeringInclusion) -> Self {
self.inclusion = inclusion;
self
}
pub fn with_content(mut self, content: impl Into<String>) -> Self {
self.content = content.into();
self
}
pub fn with_priority(mut self, priority: u8) -> Self {
self.priority = priority;
self
}
pub fn with_source_path(mut self, path: impl Into<PathBuf>) -> Self {
self.source_path = Some(path.into());
self
}
pub fn with_file_reference(mut self, reference: FileReference) -> Self {
self.file_references.push(reference);
self
}
pub fn applies_to(&self, context: &AgentContext) -> bool {
match &self.inclusion {
SteeringInclusion::Always => true,
SteeringInclusion::FileMatch { pattern } => {
if let Some(file_path) = &context.file_path {
glob_match(pattern, &file_path.to_string_lossy())
} else {
false
}
}
SteeringInclusion::Manual { key } => context.manual_keys.contains(key),
}
}
pub fn resolved_content(&self) -> String {
let mut content = self.content.clone();
for reference in &self.file_references {
if let Some(resolved) = &reference.resolved_content {
content = content.replace(&reference.syntax, resolved);
}
}
content
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum SteeringInclusion {
Always,
FileMatch {
pattern: String,
},
Manual {
key: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileReference {
pub syntax: String,
pub path: PathBuf,
pub resolved_content: Option<String>,
}
impl FileReference {
pub fn new(syntax: impl Into<String>, path: impl Into<PathBuf>) -> Self {
Self {
syntax: syntax.into(),
path: path.into(),
resolved_content: None,
}
}
pub fn with_resolved_content(mut self, content: impl Into<String>) -> Self {
self.resolved_content = Some(content.into());
self
}
}
#[derive(Debug, Clone, Default)]
pub struct AgentContext {
pub file_path: Option<PathBuf>,
pub directory: Option<PathBuf>,
pub manual_keys: Vec<String>,
pub variables: HashMap<String, String>,
}
impl AgentContext {
pub fn new() -> Self {
Self::default()
}
pub fn with_file(mut self, path: impl Into<PathBuf>) -> Self {
self.file_path = Some(path.into());
self
}
pub fn with_directory(mut self, path: impl Into<PathBuf>) -> Self {
self.directory = Some(path.into());
self
}
pub fn with_manual_key(mut self, key: impl Into<String>) -> Self {
self.manual_keys.push(key.into());
self
}
pub fn with_variable(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.variables.insert(key.into(), value.into());
self
}
}
pub struct SteeringEngine {
rules: Vec<SteeringRule>,
steering_dir: PathBuf,
inheritance_cache: HashMap<PathBuf, Vec<String>>,
}
impl SteeringEngine {
pub fn new() -> Self {
Self {
rules: Vec::new(),
steering_dir: PathBuf::from(".driven/steering"),
inheritance_cache: HashMap::new(),
}
}
pub fn with_steering_dir(steering_dir: impl Into<PathBuf>) -> Self {
Self {
rules: Vec::new(),
steering_dir: steering_dir.into(),
inheritance_cache: HashMap::new(),
}
}
pub fn register_rule(&mut self, rule: SteeringRule) -> Result<()> {
if self.rules.iter().any(|r| r.id == rule.id) {
return Err(DrivenError::Config(format!(
"Steering rule with ID '{}' already exists",
rule.id
)));
}
self.rules.push(rule);
self.rules.sort_by_key(|r| r.priority);
Ok(())
}
pub fn unregister_rule(&mut self, id: &str) -> Result<SteeringRule> {
let pos = self.rules.iter().position(|r| r.id == id).ok_or_else(|| {
DrivenError::Config(format!("Steering rule with ID '{}' not found", id))
})?;
self.inheritance_cache.clear();
Ok(self.rules.remove(pos))
}
pub fn get_rule(&self, id: &str) -> Option<&SteeringRule> {
self.rules.iter().find(|r| r.id == id)
}
pub fn get_rule_mut(&mut self, id: &str) -> Option<&mut SteeringRule> {
self.rules.iter_mut().find(|r| r.id == id)
}
pub fn list_rules(&self) -> &[SteeringRule] {
&self.rules
}
pub fn get_rules_for_context(&self, context: &AgentContext) -> Vec<&SteeringRule> {
self.rules
.iter()
.filter(|r| r.applies_to(context))
.collect()
}
pub fn load_steering(&mut self, path: &Path) -> Result<usize> {
if !path.exists() {
return Ok(0);
}
let mut count = 0;
for entry in std::fs::read_dir(path).map_err(DrivenError::Io)? {
let entry = entry.map_err(DrivenError::Io)?;
let path = entry.path();
if path.extension().is_some_and(|e| e == "md") {
match self.load_steering_file(&path) {
Ok(rule) => {
if let Err(e) = self.register_rule(rule) {
tracing::warn!(
"Failed to register steering rule from {:?}: {}",
path,
e
);
} else {
count += 1;
}
}
Err(e) => {
tracing::warn!("Failed to load steering rule from {:?}: {}", path, e);
}
}
}
}
Ok(count)
}
fn load_steering_file(&self, path: &Path) -> Result<SteeringRule> {
let content = std::fs::read_to_string(path).map_err(DrivenError::Io)?;
let (front_matter, body) = Self::parse_front_matter(&content)?;
let id = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown")
.to_string();
let inclusion = Self::parse_inclusion(&front_matter)?;
let priority = front_matter
.get("priority")
.and_then(|v| v.parse::<u8>().ok())
.unwrap_or(100);
let file_references = Self::parse_file_references(&body);
let name = front_matter
.get("name")
.cloned()
.unwrap_or_else(|| id.clone());
Ok(SteeringRule {
id,
name,
inclusion,
content: body,
file_references,
priority,
source_path: Some(path.to_path_buf()),
})
}
fn parse_front_matter(content: &str) -> Result<(HashMap<String, String>, String)> {
let content = content.trim();
if !content.starts_with("---") {
return Ok((HashMap::new(), content.to_string()));
}
let rest = &content[3..];
let end_pos = rest
.find("---")
.ok_or_else(|| DrivenError::Parse("Unclosed front matter".to_string()))?;
let front_matter_str = &rest[..end_pos].trim();
let body = rest[end_pos + 3..].trim().to_string();
let mut front_matter = HashMap::new();
for line in front_matter_str.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some((key, value)) = line.split_once(':') {
let key = key.trim().to_string();
let value = value
.trim()
.trim_matches('"')
.trim_matches('\'')
.to_string();
front_matter.insert(key, value);
}
}
Ok((front_matter, body))
}
fn parse_inclusion(front_matter: &HashMap<String, String>) -> Result<SteeringInclusion> {
let inclusion_type = front_matter
.get("inclusion")
.map(|s| s.as_str())
.unwrap_or("always");
match inclusion_type.to_lowercase().as_str() {
"always" => Ok(SteeringInclusion::Always),
"filematch" | "file_match" => {
let pattern = front_matter
.get("fileMatchPattern")
.or_else(|| front_matter.get("file_match_pattern"))
.or_else(|| front_matter.get("pattern"))
.ok_or_else(|| {
DrivenError::Parse(
"fileMatch inclusion requires fileMatchPattern".to_string(),
)
})?;
Ok(SteeringInclusion::FileMatch {
pattern: pattern.clone(),
})
}
"manual" => {
let key = front_matter
.get("key")
.or_else(|| front_matter.get("manualKey"))
.ok_or_else(|| {
DrivenError::Parse("manual inclusion requires key".to_string())
})?;
Ok(SteeringInclusion::Manual { key: key.clone() })
}
_ => Err(DrivenError::Parse(format!(
"Unknown inclusion type: {}. Valid: always, fileMatch, manual",
inclusion_type
))),
}
}
fn parse_file_references(content: &str) -> Vec<FileReference> {
let mut references = Vec::new();
let pattern = "#[[file:";
let mut pos = 0;
while let Some(start) = content[pos..].find(pattern) {
let abs_start = pos + start;
let ref_start = abs_start + pattern.len();
if let Some(end) = content[ref_start..].find("]]") {
let path_str = &content[ref_start..ref_start + end];
let syntax = format!("#[[file:{}]]", path_str);
references.push(FileReference::new(syntax, PathBuf::from(path_str)));
pos = ref_start + end + 2;
} else {
break;
}
}
references
}
pub fn resolve_file_references(&self, rule: &mut SteeringRule, base_path: &Path) -> Result<()> {
for reference in &mut rule.file_references {
let full_path = if reference.path.is_absolute() {
reference.path.clone()
} else {
base_path.join(&reference.path)
};
match std::fs::read_to_string(&full_path) {
Ok(content) => {
reference.resolved_content = Some(content);
}
Err(e) => {
tracing::warn!("Failed to resolve file reference {:?}: {}", full_path, e);
}
}
}
Ok(())
}
pub fn resolve_all_file_references(&mut self, base_path: &Path) -> Result<()> {
for rule in &mut self.rules {
for reference in &mut rule.file_references {
let full_path = if reference.path.is_absolute() {
reference.path.clone()
} else {
base_path.join(&reference.path)
};
match std::fs::read_to_string(&full_path) {
Ok(content) => {
reference.resolved_content = Some(content);
}
Err(e) => {
tracing::warn!("Failed to resolve file reference {:?}: {}", full_path, e);
}
}
}
}
Ok(())
}
pub fn get_rules_with_inheritance(&self, directory: &Path) -> Vec<&SteeringRule> {
let mut applicable_rules: Vec<&SteeringRule> = Vec::new();
let mut seen_ids: std::collections::HashSet<&str> = std::collections::HashSet::new();
let mut current = Some(directory);
while let Some(dir) = current {
let steering_dir = dir.join(".driven/steering");
for rule in &self.rules {
if let Some(source) = &rule.source_path {
if source.starts_with(&steering_dir) && !seen_ids.contains(rule.id.as_str()) {
applicable_rules.push(rule);
seen_ids.insert(&rule.id);
}
}
}
current = dir.parent();
}
applicable_rules.sort_by_key(|r| r.priority);
applicable_rules
}
pub fn inject_into_context(&self, context: &AgentContext) -> String {
let rules = self.get_rules_for_context(context);
let mut output = String::new();
for rule in rules {
if !output.is_empty() {
output.push_str("\n\n---\n\n");
}
output.push_str(&rule.resolved_content());
}
output
}
pub fn save_rule(&self, rule: &SteeringRule, path: &Path) -> Result<()> {
let mut content = String::new();
content.push_str("---\n");
content.push_str(&format!("name: \"{}\"\n", rule.name));
match &rule.inclusion {
SteeringInclusion::Always => {
content.push_str("inclusion: always\n");
}
SteeringInclusion::FileMatch { pattern } => {
content.push_str("inclusion: fileMatch\n");
content.push_str(&format!("fileMatchPattern: \"{}\"\n", pattern));
}
SteeringInclusion::Manual { key } => {
content.push_str("inclusion: manual\n");
content.push_str(&format!("key: \"{}\"\n", key));
}
}
content.push_str(&format!("priority: {}\n", rule.priority));
content.push_str("---\n\n");
content.push_str(&rule.content);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(DrivenError::Io)?;
}
std::fs::write(path, content).map_err(DrivenError::Io)?;
Ok(())
}
pub fn steering_dir(&self) -> &Path {
&self.steering_dir
}
}
impl Default for SteeringEngine {
fn default() -> Self {
Self::new()
}
}
fn glob_match(pattern: &str, text: &str) -> bool {
if pattern.contains("**") {
let parts: Vec<&str> = pattern.split("**").collect();
if parts.len() == 2 {
let prefix = parts[0].trim_end_matches('/');
let suffix = parts[1].trim_start_matches('/');
if !prefix.is_empty() && !text.starts_with(prefix) {
return false;
}
if !suffix.is_empty() && !glob_match(suffix, text.rsplit('/').next().unwrap_or(text)) {
return false;
}
return true;
}
}
if pattern.contains('*') && !pattern.contains("**") {
let parts: Vec<&str> = pattern.split('*').collect();
let mut pos = 0;
for (i, part) in parts.iter().enumerate() {
if part.is_empty() {
continue;
}
if i == 0 {
if !text.starts_with(part) {
return false;
}
pos = part.len();
} else if i == parts.len() - 1 {
if !text.ends_with(part) {
return false;
}
} else {
if let Some(found) = text[pos..].find(part) {
pos += found + part.len();
} else {
return false;
}
}
}
return true;
}
pattern == text
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_steering_rule_creation() {
let rule = SteeringRule::new("test-rule")
.with_name("Test Rule")
.with_inclusion(SteeringInclusion::Always)
.with_content("# Test Content")
.with_priority(50);
assert_eq!(rule.id, "test-rule");
assert_eq!(rule.name, "Test Rule");
assert_eq!(rule.content, "# Test Content");
assert_eq!(rule.priority, 50);
}
#[test]
fn test_always_inclusion() {
let rule = SteeringRule::new("always-rule").with_inclusion(SteeringInclusion::Always);
let context = AgentContext::new();
assert!(rule.applies_to(&context));
let context_with_file = AgentContext::new().with_file(Path::new("src/main.rs"));
assert!(rule.applies_to(&context_with_file));
}
#[test]
fn test_file_match_inclusion() {
let rule = SteeringRule::new("rust-rule").with_inclusion(SteeringInclusion::FileMatch {
pattern: "**/*.rs".to_string(),
});
let rust_context = AgentContext::new().with_file(Path::new("src/main.rs"));
assert!(rule.applies_to(&rust_context));
let python_context = AgentContext::new().with_file(Path::new("src/main.py"));
assert!(!rule.applies_to(&python_context));
let no_file_context = AgentContext::new();
assert!(!rule.applies_to(&no_file_context));
}
#[test]
fn test_manual_inclusion() {
let rule = SteeringRule::new("manual-rule").with_inclusion(SteeringInclusion::Manual {
key: "rust-style".to_string(),
});
let context_without_key = AgentContext::new();
assert!(!rule.applies_to(&context_without_key));
let context_with_key = AgentContext::new().with_manual_key("rust-style");
assert!(rule.applies_to(&context_with_key));
let context_with_wrong_key = AgentContext::new().with_manual_key("python-style");
assert!(!rule.applies_to(&context_with_wrong_key));
}
#[test]
fn test_file_reference_parsing() {
let content = r#"
# Rust Style Guide
Follow these rules:
#[[file:docs/rust-style.md]]
Also see:
#[[file:Cargo.toml]]
"#;
let references = SteeringEngine::parse_file_references(content);
assert_eq!(references.len(), 2);
assert_eq!(references[0].path, PathBuf::from("docs/rust-style.md"));
assert_eq!(references[1].path, PathBuf::from("Cargo.toml"));
}
#[test]
fn test_front_matter_parsing() {
let content = r#"---
name: "Rust Standards"
inclusion: fileMatch
fileMatchPattern: "**/*.rs"
priority: 10
---
# Rust Code Standards
Use these standards when writing Rust code.
"#;
let (front_matter, body) = SteeringEngine::parse_front_matter(content).unwrap();
assert_eq!(
front_matter.get("name"),
Some(&"Rust Standards".to_string())
);
assert_eq!(
front_matter.get("inclusion"),
Some(&"fileMatch".to_string())
);
assert_eq!(
front_matter.get("fileMatchPattern"),
Some(&"**/*.rs".to_string())
);
assert_eq!(front_matter.get("priority"), Some(&"10".to_string()));
assert!(body.contains("# Rust Code Standards"));
}
#[test]
fn test_front_matter_parsing_no_front_matter() {
let content = "# Just Content\n\nNo front matter here.";
let (front_matter, body) = SteeringEngine::parse_front_matter(content).unwrap();
assert!(front_matter.is_empty());
assert_eq!(body, content);
}
#[test]
fn test_steering_engine_register() {
let mut engine = SteeringEngine::new();
let rule = SteeringRule::new("test-rule");
engine.register_rule(rule).unwrap();
assert_eq!(engine.list_rules().len(), 1);
assert!(engine.get_rule("test-rule").is_some());
}
#[test]
fn test_steering_engine_duplicate_id() {
let mut engine = SteeringEngine::new();
let rule1 = SteeringRule::new("test-rule");
let rule2 = SteeringRule::new("test-rule");
engine.register_rule(rule1).unwrap();
assert!(engine.register_rule(rule2).is_err());
}
#[test]
fn test_steering_engine_unregister() {
let mut engine = SteeringEngine::new();
let rule = SteeringRule::new("test-rule");
engine.register_rule(rule).unwrap();
let removed = engine.unregister_rule("test-rule").unwrap();
assert_eq!(removed.id, "test-rule");
assert!(engine.get_rule("test-rule").is_none());
}
#[test]
fn test_steering_engine_priority_ordering() {
let mut engine = SteeringEngine::new();
let low_priority = SteeringRule::new("low").with_priority(200);
let high_priority = SteeringRule::new("high").with_priority(50);
let medium_priority = SteeringRule::new("medium").with_priority(100);
engine.register_rule(low_priority).unwrap();
engine.register_rule(high_priority).unwrap();
engine.register_rule(medium_priority).unwrap();
let rules = engine.list_rules();
assert_eq!(rules[0].id, "high");
assert_eq!(rules[1].id, "medium");
assert_eq!(rules[2].id, "low");
}
#[test]
fn test_steering_engine_get_rules_for_context() {
let mut engine = SteeringEngine::new();
let always_rule = SteeringRule::new("always").with_inclusion(SteeringInclusion::Always);
let rust_rule = SteeringRule::new("rust").with_inclusion(SteeringInclusion::FileMatch {
pattern: "**/*.rs".to_string(),
});
let manual_rule = SteeringRule::new("manual").with_inclusion(SteeringInclusion::Manual {
key: "special".to_string(),
});
engine.register_rule(always_rule).unwrap();
engine.register_rule(rust_rule).unwrap();
engine.register_rule(manual_rule).unwrap();
let rust_context = AgentContext::new().with_file(Path::new("src/main.rs"));
let rules = engine.get_rules_for_context(&rust_context);
assert_eq!(rules.len(), 2);
let python_context = AgentContext::new().with_file(Path::new("src/main.py"));
let rules = engine.get_rules_for_context(&python_context);
assert_eq!(rules.len(), 1);
let manual_context = AgentContext::new().with_manual_key("special");
let rules = engine.get_rules_for_context(&manual_context);
assert_eq!(rules.len(), 2); }
#[test]
fn test_resolved_content() {
let mut rule = SteeringRule::new("test").with_content("See #[[file:test.md]] for details.");
rule.file_references.push(
FileReference::new("#[[file:test.md]]", "test.md")
.with_resolved_content("# Test Content"),
);
let resolved = rule.resolved_content();
assert_eq!(resolved, "See # Test Content for details.");
}
#[test]
fn test_glob_match_star() {
assert!(glob_match("*.rs", "main.rs"));
assert!(glob_match("*.rs", "lib.rs"));
assert!(!glob_match("*.rs", "main.py"));
assert!(glob_match("test_*", "test_something"));
assert!(!glob_match("test_*", "something_test"));
}
#[test]
fn test_glob_match_double_star() {
assert!(glob_match("**/*.rs", "src/main.rs"));
assert!(glob_match("**/*.rs", "src/lib/mod.rs"));
assert!(glob_match("**/*.rs", "main.rs"));
assert!(!glob_match("**/*.rs", "main.py"));
}
#[test]
fn test_glob_match_exact() {
assert!(glob_match("main.rs", "main.rs"));
assert!(!glob_match("main.rs", "lib.rs"));
}
#[test]
fn test_agent_context() {
let context = AgentContext::new()
.with_file(Path::new("src/main.rs"))
.with_directory(Path::new("src"))
.with_manual_key("rust-style")
.with_variable("project", "driven");
assert_eq!(context.file_path, Some(PathBuf::from("src/main.rs")));
assert_eq!(context.directory, Some(PathBuf::from("src")));
assert!(context.manual_keys.contains(&"rust-style".to_string()));
assert_eq!(
context.variables.get("project"),
Some(&"driven".to_string())
);
}
#[test]
fn test_inject_into_context() {
let mut engine = SteeringEngine::new();
let rule1 = SteeringRule::new("rule1")
.with_inclusion(SteeringInclusion::Always)
.with_content("# Rule 1 Content")
.with_priority(10);
let rule2 = SteeringRule::new("rule2")
.with_inclusion(SteeringInclusion::Always)
.with_content("# Rule 2 Content")
.with_priority(20);
engine.register_rule(rule1).unwrap();
engine.register_rule(rule2).unwrap();
let context = AgentContext::new();
let output = engine.inject_into_context(&context);
assert!(output.contains("# Rule 1 Content"));
assert!(output.contains("# Rule 2 Content"));
assert!(output.contains("---")); }
}
#[cfg(test)]
mod property_tests {
use super::*;
use proptest::prelude::*;
fn arb_extension() -> impl Strategy<Value = String> {
prop_oneof![
Just("rs".to_string()),
Just("py".to_string()),
Just("js".to_string()),
Just("ts".to_string()),
Just("md".to_string()),
Just("json".to_string()),
Just("yaml".to_string()),
]
}
fn arb_file_path() -> impl Strategy<Value = PathBuf> {
(
prop_oneof![Just("src"), Just("tests"), Just("lib"), Just("bin"),],
"[a-z_]+",
arb_extension(),
)
.prop_map(|(dir, name, ext)| PathBuf::from(format!("{}/{}.{}", dir, name, ext)))
}
fn arb_inclusion() -> impl Strategy<Value = SteeringInclusion> {
prop_oneof![
Just(SteeringInclusion::Always),
arb_extension().prop_map(|ext| SteeringInclusion::FileMatch {
pattern: format!("**/*.{}", ext),
}),
"[a-z_]+".prop_map(|key| SteeringInclusion::Manual { key }),
]
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_always_rules_always_apply(
path in arb_file_path(),
content in "[a-zA-Z ]+",
) {
let mut engine = SteeringEngine::new();
let rule = SteeringRule::new("always-rule")
.with_inclusion(SteeringInclusion::Always)
.with_content(&content);
engine.register_rule(rule).unwrap();
let context = AgentContext::new().with_file(&path);
let rules = engine.get_rules_for_context(&context);
prop_assert_eq!(rules.len(), 1);
prop_assert_eq!(&rules[0].id, "always-rule");
let empty_context = AgentContext::new();
let rules = engine.get_rules_for_context(&empty_context);
prop_assert_eq!(rules.len(), 1);
}
#[test]
fn prop_file_match_rules_apply_on_match(
ext in arb_extension(),
dir in prop_oneof![Just("src"), Just("tests"), Just("lib")],
name in "[a-z_]+",
) {
let mut engine = SteeringEngine::new();
let pattern = format!("**/*.{}", ext);
let rule = SteeringRule::new("file-rule")
.with_inclusion(SteeringInclusion::FileMatch { pattern });
engine.register_rule(rule).unwrap();
let matching_path = PathBuf::from(format!("{}/{}.{}", dir, name, ext));
let context = AgentContext::new().with_file(&matching_path);
let rules = engine.get_rules_for_context(&context);
prop_assert_eq!(rules.len(), 1);
let non_matching_path = PathBuf::from(format!("{}/{}.different", dir, name));
let context = AgentContext::new().with_file(&non_matching_path);
let rules = engine.get_rules_for_context(&context);
prop_assert_eq!(rules.len(), 0);
}
#[test]
fn prop_manual_rules_apply_on_key(
key in "[a-z_]+",
other_key in "[a-z_]+",
) {
let mut engine = SteeringEngine::new();
let rule = SteeringRule::new("manual-rule")
.with_inclusion(SteeringInclusion::Manual { key: key.clone() });
engine.register_rule(rule).unwrap();
let context = AgentContext::new().with_manual_key(&key);
let rules = engine.get_rules_for_context(&context);
prop_assert_eq!(rules.len(), 1);
if key != other_key {
let context = AgentContext::new().with_manual_key(&other_key);
let rules = engine.get_rules_for_context(&context);
prop_assert_eq!(rules.len(), 0);
}
}
#[test]
fn prop_file_references_are_parsed(
path1 in "[a-z_/]+\\.md",
path2 in "[a-z_/]+\\.txt",
) {
let content = format!(
"See #[[file:{}]] and #[[file:{}]] for details.",
path1, path2
);
let references = SteeringEngine::parse_file_references(&content);
prop_assert_eq!(references.len(), 2);
prop_assert_eq!(references[0].path.clone(), PathBuf::from(&path1));
prop_assert_eq!(references[1].path.clone(), PathBuf::from(&path2));
}
#[test]
fn prop_steering_rule_roundtrip(
id in "[a-z_]+",
name in "[a-zA-Z ]+",
content in "[a-zA-Z ]+",
priority in 0u8..255,
) {
let rule = SteeringRule::new(id.clone())
.with_name(name.clone())
.with_inclusion(SteeringInclusion::Always)
.with_content(content.clone())
.with_priority(priority);
let json = serde_json::to_string(&rule).expect("Should serialize");
let loaded: SteeringRule = serde_json::from_str(&json).expect("Should deserialize");
prop_assert_eq!(loaded.id, id);
prop_assert_eq!(loaded.name, name);
prop_assert_eq!(loaded.content, content);
prop_assert_eq!(loaded.priority, priority);
prop_assert_eq!(loaded.inclusion, SteeringInclusion::Always);
}
}
}