Skip to main content

smos_application/helpers/
memory_block.rs

1//! Memory block builder — render the `<smos-memory>` injection block (§3 step 8).
2//!
3//! The block carries:
4//! - An opening tag with the active `session_id`.
5//! - Zero or more `[fact_id] document` lines.
6//! - A closing tag.
7//!
8//! Persona lines are intentionally NOT rendered here: the persona lives behind
9//! an IO boundary (adapter reads `memory_key/persona.md`). Slice 1 builds the
10//! facts-only block; the adapter layer will prepend persona lines when present.
11
12use smos_domain::{FactId, MemoryKey, SessionId};
13
14/// One fact to render in the memory block.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct MemoryBlockEntry<'a> {
17    pub id: &'a FactId,
18    pub document: &'a str,
19}
20
21/// Build the `<smos-memory>` block.
22///
23/// Format:
24/// ```text
25/// <smos-memory session="sess_...">
26/// [fact_xxx] document text
27/// [fact_yyy] document text
28/// </smos-memory>
29/// ```
30///
31/// `memory_key` is accepted for forward compatibility with the persona
32/// (`[persona-...]` lines) but not currently rendered; slice 1 emits facts only.
33pub fn build<'a>(
34    facts: impl IntoIterator<Item = MemoryBlockEntry<'a>>,
35    session_id: &SessionId,
36    _memory_key: &MemoryKey,
37) -> String {
38    let mut lines: Vec<String> = Vec::new();
39    lines.push(format!("<smos-memory session=\"{}\">", session_id.as_str()));
40    for entry in facts {
41        lines.push(format!("[{}] {}", entry.id.as_str(), entry.document));
42    }
43    lines.push("</smos-memory>".to_string());
44    lines.join("\n")
45}
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50
51    fn sid() -> SessionId {
52        SessionId::from_raw("sess_abcdef012345").unwrap()
53    }
54
55    fn key() -> MemoryKey {
56        MemoryKey::from_raw("origa").unwrap()
57    }
58
59    fn fid(content: &str) -> FactId {
60        FactId::from_content(content)
61    }
62
63    #[test]
64    fn empty_facts_emits_open_and_close_tags() {
65        let block = build([], &sid(), &key());
66        assert_eq!(
67            block,
68            "<smos-memory session=\"sess_abcdef012345\">\n</smos-memory>"
69        );
70    }
71
72    #[test]
73    fn each_fact_gets_one_line_with_id_and_document() {
74        let id1 = fid("first fact");
75        let id2 = fid("second fact");
76        let facts = vec![
77            MemoryBlockEntry {
78                id: &id1,
79                document: "First fact text",
80            },
81            MemoryBlockEntry {
82                id: &id2,
83                document: "Second fact text",
84            },
85        ];
86        let block = build(facts, &sid(), &key());
87        let lines: Vec<&str> = block.lines().collect();
88        assert_eq!(lines.len(), 4);
89        assert!(lines[1].starts_with(&format!("[{}]", id1.as_str())));
90        assert!(lines[1].contains("First fact text"));
91        assert!(lines[2].starts_with(&format!("[{}]", id2.as_str())));
92        assert!(lines[2].contains("Second fact text"));
93        assert_eq!(lines[3], "</smos-memory>");
94    }
95
96    #[test]
97    fn opening_tag_carries_session_id_attribute() {
98        let block = build([], &sid(), &key());
99        assert!(block.starts_with("<smos-memory session=\"sess_abcdef012345\">"));
100    }
101
102    #[test]
103    fn closing_tag_is_emitted_last() {
104        let id = fid("x");
105        let facts = vec![MemoryBlockEntry {
106            id: &id,
107            document: "y",
108        }];
109        let block = build(facts, &sid(), &key());
110        assert!(block.ends_with("</smos-memory>"));
111    }
112}