Skip to main content

cel_memory/
conformance.rs

1//! Shared behavioral checks for [`MemoryProvider`] implementations.
2//!
3//! Backend crates should call these helpers from integration tests so every
4//! persistence layer honors the same contract. See
5//! [`cel-memory-sqlite/tests/swap.rs`](https://github.com/dimpagk92/cel-memory-sqlite/blob/main/tests/swap.rs)
6//! for a downstream example.
7
8use std::sync::Arc;
9
10use crate::{
11    CallerScope, ChunkKind, ChunkSource, MemoryChunk, MemoryProvider, MemoryQuery, MemorySession,
12    MemoryStats, NewMemoryChunk, NewMemorySession, Result, RetrievalProfile, SessionOutcome,
13};
14
15/// Minimal contract every backend must satisfy: write a chunk, read it back,
16/// and report coherent stats.
17pub async fn assert_write_get_stats(
18    memory: Arc<dyn MemoryProvider>,
19    content: &str,
20) -> Result<(MemoryChunk, MemoryStats)> {
21    let chunk = memory
22        .write(NewMemoryChunk {
23            kind: ChunkKind::Chat,
24            source: ChunkSource::Embedded,
25            session_id: None,
26            project_root: None,
27            caller_id: "conformance".into(),
28            content: content.into(),
29            metadata: serde_json::Value::Null,
30            importance: None,
31            shareable: false,
32            pinned: false,
33        })
34        .await?;
35
36    let fetched = memory
37        .get(&chunk.id)
38        .await?
39        .ok_or_else(|| crate::MemoryError::NotFound(chunk.id.clone()))?;
40    assert_eq!(fetched.content, chunk.content);
41
42    let stats = memory.stats().await?;
43    assert!(stats.total_chunks >= 1);
44
45    Ok((chunk, stats))
46}
47
48/// Hybrid retrieval must surface a chunk the provider just wrote.
49pub async fn assert_retrieve_finds_written(
50    memory: Arc<dyn MemoryProvider>,
51    content: &str,
52) -> Result<MemoryChunk> {
53    let chunk = memory
54        .write(NewMemoryChunk {
55            kind: ChunkKind::Chat,
56            source: ChunkSource::Embedded,
57            session_id: None,
58            project_root: None,
59            caller_id: "conformance".into(),
60            content: content.into(),
61            metadata: serde_json::Value::Null,
62            importance: None,
63            shareable: false,
64            pinned: false,
65        })
66        .await?;
67
68    let hits = memory
69        .retrieve(MemoryQuery {
70            text: content.into(),
71            kinds: Some(vec![ChunkKind::Chat]),
72            since: None,
73            until: None,
74            session_id: None,
75            caller_scope: CallerScope::Global,
76            project_root_prefix: None,
77            k: 8,
78            include_rollups: true,
79            min_importance: None,
80            profile: RetrievalProfile::AgentChatTurn,
81            caller_id: "conformance".into(),
82        })
83        .await?;
84
85    assert!(
86        hits.iter().any(|hit| hit.id == chunk.id),
87        "retrieve did not return the written chunk; got {} hits",
88        hits.len()
89    );
90
91    Ok(chunk)
92}
93
94/// Session open → scoped write → close must round-trip through `get_session`.
95pub async fn assert_session_lifecycle(memory: Arc<dyn MemoryProvider>) -> Result<MemorySession> {
96    let session = memory
97        .open_session(NewMemorySession {
98            caller_id: "conformance".into(),
99            title: Some("conformance session".into()),
100            metadata: serde_json::Value::Null,
101        })
102        .await?;
103
104    memory
105        .write(NewMemoryChunk {
106            kind: ChunkKind::Chat,
107            source: ChunkSource::Embedded,
108            session_id: Some(session.id.clone()),
109            project_root: None,
110            caller_id: session.caller_id.clone(),
111            content: "session-scoped turn".into(),
112            metadata: serde_json::Value::Null,
113            importance: None,
114            shareable: false,
115            pinned: false,
116        })
117        .await?;
118
119    let open = memory
120        .get_session(&session.id)
121        .await?
122        .ok_or_else(|| crate::MemoryError::NotFound(session.id.clone()))?;
123    assert_eq!(open.outcome, SessionOutcome::Open);
124
125    memory
126        .close_session(&session.id, SessionOutcome::Success)
127        .await?;
128
129    let closed = memory
130        .get_session(&session.id)
131        .await?
132        .ok_or_else(|| crate::MemoryError::NotFound(session.id.clone()))?;
133    assert_eq!(closed.outcome, SessionOutcome::Success);
134
135    Ok(closed)
136}
137
138/// Summarization must produce a persisted `JobSummary` linked to the session.
139///
140/// Backends without summarization support should skip calling this helper.
141pub async fn assert_summarize_session_roundtrip(
142    memory: Arc<dyn MemoryProvider>,
143) -> Result<MemoryChunk> {
144    let session = memory
145        .open_session(NewMemorySession {
146            caller_id: "conformance".into(),
147            title: Some("summarize me".into()),
148            metadata: serde_json::Value::Null,
149        })
150        .await?;
151
152    memory
153        .write(NewMemoryChunk {
154            kind: ChunkKind::Chat,
155            source: ChunkSource::Embedded,
156            session_id: Some(session.id.clone()),
157            project_root: None,
158            caller_id: session.caller_id.clone(),
159            content: "first turn in session".into(),
160            metadata: serde_json::Value::Null,
161            importance: None,
162            shareable: false,
163            pinned: false,
164        })
165        .await?;
166    memory
167        .write(NewMemoryChunk {
168            kind: ChunkKind::Chat,
169            source: ChunkSource::Embedded,
170            session_id: Some(session.id.clone()),
171            project_root: None,
172            caller_id: session.caller_id.clone(),
173            content: "second turn in session".into(),
174            metadata: serde_json::Value::Null,
175            importance: None,
176            shareable: false,
177            pinned: false,
178        })
179        .await?;
180
181    let summary = memory.summarize_session(&session.id).await?;
182    assert_eq!(summary.kind, ChunkKind::JobSummary);
183    assert!(
184        memory.get(&summary.id).await?.is_some(),
185        "summary chunk was not persisted"
186    );
187
188    Ok(summary)
189}