1use crate::backend::contract::{CallContext, MemoryBackend};
4use crate::backend::error::{MemoryBackendError, MemoryBackendErrorKind};
5use crate::backend::types::{BackendKind, MemoryTier};
6use async_trait::async_trait;
7use std::time::Duration;
8use tokio_util::sync::CancellationToken;
9
10pub const REQUIRED_OPERATIONS: &[&str] = &[
11 "activity_json",
12 "delete",
13 "embedder_dim",
14 "entries",
15 "get_full",
16 "log_recall_with_replay",
17 "metrics_json",
18 "put",
19 "recall_scored",
20 "record_injections",
21 "remember",
22 "scopes",
23 "search",
24 "try_put",
25 "list",
26 "remember_consolidated",
27];
28
29pub const REQUIRED_ERROR_KINDS: &[MemoryBackendErrorKind] = &[
30 MemoryBackendErrorKind::Unavailable,
31 MemoryBackendErrorKind::Invalid,
32 MemoryBackendErrorKind::Denied,
33 MemoryBackendErrorKind::Timeout,
34 MemoryBackendErrorKind::Cancelled,
35 MemoryBackendErrorKind::Incompatible,
36 MemoryBackendErrorKind::Store,
37 MemoryBackendErrorKind::Internal,
38];
39
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct OperationObservation {
42 pub operation: String,
43 pub error_kind: Option<MemoryBackendErrorKind>,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct ConformanceFailure {
48 pub operation: String,
49 pub expected: Option<MemoryBackendErrorKind>,
51 pub observed: Option<MemoryBackendErrorKind>,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct TypedErrorObservation {
56 pub error_name: String,
57 pub expected: MemoryBackendErrorKind,
58 pub observed: Option<MemoryBackendErrorKind>,
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct ConformanceReport {
63 pub backend: BackendKind,
64 pub operations: Vec<OperationObservation>,
65 pub failures: Vec<ConformanceFailure>,
66 pub typed_errors: Vec<TypedErrorObservation>,
67 pub cancellation_kind: Option<MemoryBackendErrorKind>,
68 pub deadline_kind: Option<MemoryBackendErrorKind>,
69 pub local_open_count: usize,
70 pub no_local_open: bool,
71}
72
73impl ConformanceReport {
74 pub fn passed(&self) -> bool {
75 self.failures.is_empty()
76 && self.operations.len() == REQUIRED_OPERATIONS.len()
77 && self.typed_errors.len() == REQUIRED_ERROR_KINDS.len()
78 && REQUIRED_ERROR_KINDS.iter().all(|expected| {
79 self.typed_errors
80 .iter()
81 .any(|case| case.expected == *expected && case.observed == Some(*expected))
82 })
83 && self.cancellation_kind == Some(MemoryBackendErrorKind::Cancelled)
84 && self.deadline_kind == Some(MemoryBackendErrorKind::Timeout)
85 && self.no_local_open
86 }
87}
88
89#[async_trait]
92pub trait ConformanceFactory: Send + Sync {
93 type Backend: MemoryBackend;
94
95 async fn create(&self) -> Result<Self::Backend, MemoryBackendError>;
96 fn local_open_count(&self) -> usize;
97 async fn typed_error_cases(&self) -> Vec<TypedErrorObservation>;
98}
99
100fn observe<T>(operation: &str, result: Result<T, MemoryBackendError>) -> OperationObservation {
101 OperationObservation {
102 operation: operation.to_string(),
103 error_kind: result.err().map(|error| error.kind()),
104 }
105}
106
107fn expected_error(
108 operation: &str,
109 expected: Option<MemoryBackendErrorKind>,
110 observed: Option<MemoryBackendErrorKind>,
111) -> Option<ConformanceFailure> {
112 (observed != expected).then(|| ConformanceFailure {
113 operation: operation.to_string(),
114 expected,
115 observed,
116 })
117}
118
119pub async fn run_conformance<F>(factory: &F) -> Result<ConformanceReport, MemoryBackendError>
125where
126 F: ConformanceFactory,
127{
128 let backend = factory.create().await?;
129 let context = CallContext::unbounded();
130 let mut operations = Vec::with_capacity(REQUIRED_OPERATIONS.len());
131
132 operations.push(observe(
133 "activity_json",
134 backend.activity_json(&context, 20).await,
135 ));
136 operations.push(observe(
137 "delete",
138 backend.delete(&context, "conformance-missing").await,
139 ));
140 operations.push(observe(
141 "embedder_dim",
142 backend.embedder_dim(&context).await,
143 ));
144 operations.push(observe("entries", backend.entries(&context, 20).await));
145 operations.push(observe(
146 "get_full",
147 backend.get_full(&context, "conformance-missing").await,
148 ));
149 operations.push(observe(
150 "log_recall_with_replay",
151 backend
152 .log_recall_with_replay(
153 &context,
154 "1970-01-01T00:00:00Z",
155 None,
156 0,
157 0,
158 0,
159 0,
160 "conformance",
161 None,
162 None,
163 None,
164 None,
165 None,
166 None,
167 Some("actor"),
168 "test",
169 "[]",
170 "[]",
171 )
172 .await,
173 ));
174 operations.push(observe(
175 "metrics_json",
176 backend.metrics_json(&context).await,
177 ));
178 operations.push(observe(
179 "put",
180 backend
181 .put(
182 &context,
183 "conformance",
184 "fixture",
185 "global",
186 MemoryTier::Semantic,
187 )
188 .await,
189 ));
190 operations.push(observe(
191 "recall_scored",
192 backend.recall_scored(&context, "fixture", 5, &[]).await,
193 ));
194 operations.push(observe(
195 "record_injections",
196 backend.record_injections(&context, &[]).await,
197 ));
198 operations.push(observe(
199 "remember",
200 backend
201 .remember(&context, "fixture", vec!["conformance".into()])
202 .await,
203 ));
204 operations.push(observe("scopes", backend.scopes(&context).await));
205 operations.push(observe(
206 "search",
207 backend.search(&context, "fixture", 5).await,
208 ));
209 operations.push(observe(
210 "try_put",
211 backend
212 .try_put(
213 &context,
214 "conformance",
215 "fixture",
216 "global",
217 MemoryTier::Semantic,
218 )
219 .await,
220 ));
221 operations.push(observe("list", backend.list(&context, None).await));
222 operations.push(observe(
223 "remember_consolidated",
224 backend
225 .remember_consolidated(&context, "conformance", "fixture", vec![], 0.1)
226 .await,
227 ));
228
229 let cancellation = CancellationToken::new();
230 cancellation.cancel();
231 let cancelled = CallContext::unbounded().with_cancellation(cancellation);
232 let cancellation_kind = backend
233 .metrics_json(&cancelled)
234 .await
235 .err()
236 .map(|error| error.kind());
237
238 let expired = CallContext::after(Duration::ZERO);
239 let deadline_kind = backend
240 .metrics_json(&expired)
241 .await
242 .err()
243 .map(|error| error.kind());
244
245 let mut failures = Vec::new();
246 for observation in &operations {
247 let accepted_missing_row = observation.operation == "get_full"
248 && matches!(
249 observation.error_kind,
250 Some(MemoryBackendErrorKind::Store | MemoryBackendErrorKind::Invalid)
251 );
252 if observation.error_kind.is_some() && !accepted_missing_row {
253 failures.push(ConformanceFailure {
254 operation: observation.operation.clone(),
255 expected: None,
256 observed: observation.error_kind,
257 });
258 }
259 }
260 failures.extend(expected_error(
261 "cancellation",
262 Some(MemoryBackendErrorKind::Cancelled),
263 cancellation_kind,
264 ));
265 failures.extend(expected_error(
266 "deadline",
267 Some(MemoryBackendErrorKind::Timeout),
268 deadline_kind,
269 ));
270
271 let typed_errors = factory.typed_error_cases().await;
272 for case in &typed_errors {
273 if case.observed != Some(case.expected) {
274 failures.push(ConformanceFailure {
275 operation: format!("typed_error:{}", case.error_name),
276 expected: Some(case.expected),
277 observed: case.observed,
278 });
279 }
280 }
281
282 let local_open_count = factory.local_open_count();
283 let no_local_open = backend.binding().kind() != BackendKind::Service || local_open_count == 0;
284 if !no_local_open {
285 failures.push(ConformanceFailure {
286 operation: "no_local_open".into(),
287 expected: Some(MemoryBackendErrorKind::Unavailable),
288 observed: Some(MemoryBackendErrorKind::Store),
289 });
290 }
291
292 Ok(ConformanceReport {
293 backend: backend.binding().kind(),
294 operations,
295 failures,
296 typed_errors,
297 cancellation_kind,
298 deadline_kind,
299 local_open_count,
300 no_local_open,
301 })
302}