Skip to main content

agm_core/validator/
memory.rs

1//! Memory entry validation (spec S28).
2//!
3//! Pass 3 (structural): validates memory entries on a node.
4
5use crate::error::diagnostic::{AgmError, ErrorLocation};
6use crate::model::node::Node;
7
8/// Validates all memory entries on a node.
9///
10/// Rules: V022 (invalid memory key pattern), V025 (invalid topic pattern),
11/// V023 (upsert without value, search without query).
12///
13/// Delegates to `memory::schema::validate_memory_entry` for each entry.
14#[must_use]
15pub fn validate_memory(node: &Node, file_name: &str) -> Vec<AgmError> {
16    let entries = match &node.memory {
17        Some(e) => e,
18        None => return Vec::new(),
19    };
20
21    let mut errors = Vec::new();
22    let line = node.span.start_line;
23    let id = node.id.as_str();
24    let loc = ErrorLocation::full(file_name, line, id);
25
26    for entry in entries {
27        errors.extend(crate::memory::schema::validate_memory_entry(
28            entry,
29            loc.clone(),
30        ));
31    }
32
33    errors
34}
35
36#[cfg(test)]
37mod tests {
38    use std::collections::BTreeMap;
39
40    use super::*;
41    use crate::error::codes::ErrorCode;
42    use crate::model::fields::{NodeType, Span};
43    use crate::model::memory::{MemoryAction, MemoryEntry};
44    use crate::model::node::Node;
45
46    fn minimal_node() -> Node {
47        Node {
48            id: "test.node".to_owned(),
49            node_type: NodeType::Facts,
50            summary: "a test node".to_owned(),
51            priority: None,
52            stability: None,
53            confidence: None,
54            status: None,
55            depends: None,
56            related_to: None,
57            replaces: None,
58            conflicts: None,
59            see_also: None,
60            items: None,
61            steps: None,
62            fields: None,
63            input: None,
64            output: None,
65            detail: None,
66            rationale: None,
67            tradeoffs: None,
68            resolution: None,
69            examples: None,
70            notes: None,
71            code: None,
72            code_blocks: None,
73            verify: None,
74            agent_context: None,
75            target: None,
76            execution_status: None,
77            executed_by: None,
78            executed_at: None,
79            execution_log: None,
80            retry_count: None,
81            parallel_groups: None,
82            memory: None,
83            scope: None,
84            applies_when: None,
85            valid_from: None,
86            valid_until: None,
87            tags: None,
88            aliases: None,
89            keywords: None,
90            extra_fields: BTreeMap::new(),
91            span: Span::new(5, 7),
92        }
93    }
94
95    fn valid_entry() -> MemoryEntry {
96        MemoryEntry {
97            key: "repo.pattern".to_owned(),
98            topic: "rust.repository".to_owned(),
99            action: MemoryAction::Get,
100            value: None,
101            scope: None,
102            ttl: None,
103            query: None,
104            max_results: None,
105        }
106    }
107
108    #[test]
109    fn test_validate_memory_none_returns_empty() {
110        let node = minimal_node();
111        let errors = validate_memory(&node, "test.agm");
112        assert!(errors.is_empty());
113    }
114
115    #[test]
116    fn test_validate_memory_valid_entry_returns_empty() {
117        let mut node = minimal_node();
118        node.memory = Some(vec![valid_entry()]);
119        let errors = validate_memory(&node, "test.agm");
120        assert!(errors.is_empty());
121    }
122
123    #[test]
124    fn test_validate_memory_invalid_key_uppercase_returns_v022() {
125        let mut node = minimal_node();
126        let mut entry = valid_entry();
127        entry.key = "Repo.Pattern".to_owned();
128        node.memory = Some(vec![entry]);
129        let errors = validate_memory(&node, "test.agm");
130        assert!(errors.iter().any(|e| e.code == ErrorCode::V022));
131    }
132
133    #[test]
134    fn test_validate_memory_invalid_key_special_chars_returns_v022() {
135        let mut node = minimal_node();
136        let mut entry = valid_entry();
137        entry.key = "repo-pattern".to_owned(); // hyphens not allowed
138        node.memory = Some(vec![entry]);
139        let errors = validate_memory(&node, "test.agm");
140        assert!(errors.iter().any(|e| e.code == ErrorCode::V022));
141    }
142
143    #[test]
144    fn test_validate_memory_invalid_key_leading_dot_returns_v022() {
145        let mut node = minimal_node();
146        let mut entry = valid_entry();
147        entry.key = ".repo.pattern".to_owned();
148        node.memory = Some(vec![entry]);
149        let errors = validate_memory(&node, "test.agm");
150        assert!(errors.iter().any(|e| e.code == ErrorCode::V022));
151    }
152
153    #[test]
154    fn test_validate_memory_upsert_no_value_returns_v023() {
155        let mut node = minimal_node();
156        let mut entry = valid_entry();
157        entry.action = MemoryAction::Upsert;
158        entry.value = None;
159        node.memory = Some(vec![entry]);
160        let errors = validate_memory(&node, "test.agm");
161        assert!(errors.iter().any(|e| e.code == ErrorCode::V023));
162    }
163
164    #[test]
165    fn test_validate_memory_upsert_with_value_returns_empty() {
166        let mut node = minimal_node();
167        let mut entry = valid_entry();
168        entry.action = MemoryAction::Upsert;
169        entry.value = Some("the value".to_owned());
170        node.memory = Some(vec![entry]);
171        let errors = validate_memory(&node, "test.agm");
172        assert!(!errors.iter().any(|e| e.code == ErrorCode::V023));
173    }
174
175    #[test]
176    fn test_validate_memory_search_no_query_returns_v023() {
177        let mut node = minimal_node();
178        let mut entry = valid_entry();
179        entry.action = MemoryAction::Search;
180        entry.query = None;
181        node.memory = Some(vec![entry]);
182        let errors = validate_memory(&node, "test.agm");
183        assert!(errors.iter().any(|e| e.code == ErrorCode::V023));
184    }
185
186    #[test]
187    fn test_validate_memory_search_with_query_returns_empty() {
188        let mut node = minimal_node();
189        let mut entry = valid_entry();
190        entry.action = MemoryAction::Search;
191        entry.query = Some("find auth patterns".to_owned());
192        node.memory = Some(vec![entry]);
193        let errors = validate_memory(&node, "test.agm");
194        assert!(!errors.iter().any(|e| e.code == ErrorCode::V023));
195    }
196
197    #[test]
198    fn test_validate_memory_get_no_query_returns_empty() {
199        let mut node = minimal_node();
200        let entry = valid_entry(); // action is Get, no query needed
201        node.memory = Some(vec![entry]);
202        let errors = validate_memory(&node, "test.agm");
203        assert!(errors.is_empty());
204    }
205
206    #[test]
207    fn test_validate_memory_invalid_topic_returns_v025() {
208        let mut node = minimal_node();
209        let mut entry = valid_entry();
210        entry.topic = "Rust.Models".to_owned();
211        node.memory = Some(vec![entry]);
212        let errors = validate_memory(&node, "test.agm");
213        assert!(errors.iter().any(|e| e.code == ErrorCode::V025));
214    }
215
216    #[test]
217    fn test_validate_memory_valid_underscore_key_returns_empty() {
218        let mut node = minimal_node();
219        let mut entry = valid_entry();
220        entry.key = "repo.kanban_column.row_mapping_pattern".to_owned();
221        node.memory = Some(vec![entry]);
222        let errors = validate_memory(&node, "test.agm");
223        assert!(errors.is_empty());
224    }
225}