1use std::collections::HashMap;
4use std::path::Path;
5use std::sync::{Arc, Mutex};
6
7use anyhow::{Context as AnyhowContext, Result};
8use chrono::{DateTime, Utc};
9use rusqlite::{params, Connection, OptionalExtension};
10use serde_json::Value;
11
12use super::invalidation::InvalidationChecker;
13use super::types::*;
14
15const SCHEMA: &str = r#"
17CREATE TABLE IF NOT EXISTS context (
18 key TEXT PRIMARY KEY,
19 value TEXT NOT NULL,
20 namespace TEXT NOT NULL,
21 created_at TEXT NOT NULL,
22 updated_at TEXT NOT NULL,
23 expires_at TEXT,
24 git_commit TEXT,
25 file_path TEXT,
26 file_mtime INTEGER,
27 metadata TEXT
28);
29
30CREATE INDEX IF NOT EXISTS idx_context_namespace ON context(namespace);
31CREATE INDEX IF NOT EXISTS idx_context_file_path ON context(file_path);
32CREATE INDEX IF NOT EXISTS idx_context_expires_at ON context(expires_at);
33"#;
34
35pub struct ContextStore {
40 conn: Arc<Mutex<Connection>>,
41 invalidation: Arc<Mutex<InvalidationChecker>>,
42}
43
44impl ContextStore {
45 pub fn open(path: impl AsRef<Path>) -> Result<Self> {
47 let conn = Connection::open(path.as_ref())
48 .with_context(|| format!("Failed to open context store: {:?}", path.as_ref()))?;
49
50 conn.execute_batch(SCHEMA)?;
52
53 conn.execute_batch(
55 "PRAGMA journal_mode = WAL;
56 PRAGMA busy_timeout = 5000;
57 PRAGMA synchronous = NORMAL;"
58 )?;
59
60 Ok(Self {
61 conn: Arc::new(Mutex::new(conn)),
62 invalidation: Arc::new(Mutex::new(InvalidationChecker::new())),
63 })
64 }
65
66 pub fn in_memory() -> Result<Self> {
68 let conn = Connection::open_in_memory()?;
69 conn.execute_batch(SCHEMA)?;
70
71 Ok(Self {
72 conn: Arc::new(Mutex::new(conn)),
73 invalidation: Arc::new(Mutex::new(InvalidationChecker::new())),
74 })
75 }
76
77 pub fn with_git_repo(mut self, repo_path: impl AsRef<Path>) -> Result<Self> {
79 let checker = InvalidationChecker::from_git_repo(repo_path)?;
80 self.invalidation = Arc::new(Mutex::new(checker));
81 Ok(self)
82 }
83
84 pub fn refresh_git_state(&self) -> Result<()> {
86 let mut invalidation = self.invalidation.lock()
87 .map_err(|e| anyhow::anyhow!("Lock error: {}", e))?;
88 invalidation.refresh()
89 }
90
91 pub fn get(&self, key: &str) -> Result<Option<ContextEntry>> {
95 let conn = self.conn.lock()
96 .map_err(|e| anyhow::anyhow!("Lock error: {}", e))?;
97
98 let entry = conn.query_row(
99 "SELECT key, value, namespace, created_at, updated_at, expires_at,
100 git_commit, file_path, file_mtime, metadata
101 FROM context WHERE key = ?",
102 [key],
103 |row| row_to_entry(row),
104 ).optional()?;
105
106 if let Some(ref entry) = entry {
108 let invalidation = self.invalidation.lock()
109 .map_err(|e| anyhow::anyhow!("Lock error: {}", e))?;
110
111 if !invalidation.is_valid(entry) {
112 drop(invalidation);
114 drop(conn);
115 self.delete(key)?;
116 return Ok(None);
117 }
118 }
119
120 Ok(entry)
121 }
122
123 pub fn get_if_valid(&self, key: &str) -> Result<Option<ContextEntry>> {
125 let conn = self.conn.lock()
126 .map_err(|e| anyhow::anyhow!("Lock error: {}", e))?;
127
128 let entry = conn.query_row(
129 "SELECT key, value, namespace, created_at, updated_at, expires_at,
130 git_commit, file_path, file_mtime, metadata
131 FROM context WHERE key = ?",
132 [key],
133 |row| row_to_entry(row),
134 ).optional()?;
135
136 if let Some(ref entry) = entry {
137 let invalidation = self.invalidation.lock()
138 .map_err(|e| anyhow::anyhow!("Lock error: {}", e))?;
139
140 if !invalidation.is_valid(entry) {
141 return Ok(None);
142 }
143 }
144
145 Ok(entry)
146 }
147
148 pub fn set(&self, entry: ContextEntry) -> Result<()> {
150 let conn = self.conn.lock()
151 .map_err(|e| anyhow::anyhow!("Lock error: {}", e))?;
152
153 let (namespace, _) = Namespace::from_key(&entry.key);
154 let metadata_json = serde_json::to_string(&entry.metadata)?;
155
156 conn.execute(
157 "INSERT OR REPLACE INTO context
158 (key, value, namespace, created_at, updated_at, expires_at,
159 git_commit, file_path, file_mtime, metadata)
160 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
161 params![
162 entry.key,
163 entry.value.to_string(),
164 namespace.prefix(),
165 entry.created_at.to_rfc3339(),
166 entry.updated_at.to_rfc3339(),
167 entry.expires_at.map(|t| t.to_rfc3339()),
168 entry.git_commit,
169 entry.file_path,
170 entry.file_mtime,
171 metadata_json,
172 ],
173 )?;
174
175 Ok(())
176 }
177
178 pub fn set_value(&self, key: &str, value: Value) -> Result<()> {
180 let entry = ContextEntry::new(key, value);
181 self.set(entry)
182 }
183
184 pub fn delete(&self, key: &str) -> Result<bool> {
186 let conn = self.conn.lock()
187 .map_err(|e| anyhow::anyhow!("Lock error: {}", e))?;
188
189 let rows = conn.execute("DELETE FROM context WHERE key = ?", [key])?;
190 Ok(rows > 0)
191 }
192
193 pub fn delete_prefix(&self, prefix: &str) -> Result<usize> {
195 let conn = self.conn.lock()
196 .map_err(|e| anyhow::anyhow!("Lock error: {}", e))?;
197
198 let pattern = format!("{}%", prefix);
199 let rows = conn.execute("DELETE FROM context WHERE key LIKE ?", [&pattern])?;
200 Ok(rows)
201 }
202
203 pub fn exists(&self, key: &str) -> Result<bool> {
205 let conn = self.conn.lock()
206 .map_err(|e| anyhow::anyhow!("Lock error: {}", e))?;
207
208 let exists: bool = conn.query_row(
209 "SELECT 1 FROM context WHERE key = ?",
210 [key],
211 |_| Ok(true),
212 ).optional()?.unwrap_or(false);
213
214 Ok(exists)
215 }
216
217 pub fn list(&self, query: &ContextQuery) -> Result<Vec<ContextEntry>> {
219 let conn = self.conn.lock()
220 .map_err(|e| anyhow::anyhow!("Lock error: {}", e))?;
221
222 let mut sql = String::from(
223 "SELECT key, value, namespace, created_at, updated_at, expires_at,
224 git_commit, file_path, file_mtime, metadata
225 FROM context WHERE 1=1"
226 );
227
228 let mut params: Vec<String> = Vec::new();
229
230 if let Some(ref ns) = query.namespace {
231 sql.push_str(" AND namespace = ?");
232 params.push(ns.prefix().to_string());
233 }
234
235 if let Some(ref prefix) = query.prefix {
236 sql.push_str(" AND key LIKE ?");
237 params.push(format!("{}%", prefix));
238 }
239
240 if !query.include_expired {
241 sql.push_str(" AND (expires_at IS NULL OR expires_at > ?)");
242 params.push(Utc::now().to_rfc3339());
243 }
244
245 sql.push_str(" ORDER BY updated_at DESC");
246
247 if let Some(limit) = query.limit {
248 sql.push_str(&format!(" LIMIT {}", limit));
249 }
250 if let Some(offset) = query.offset {
251 sql.push_str(&format!(" OFFSET {}", offset));
252 }
253
254 let mut stmt = conn.prepare(&sql)?;
255 let params_refs: Vec<&dyn rusqlite::ToSql> = params.iter()
256 .map(|s| s as &dyn rusqlite::ToSql)
257 .collect();
258
259 let entries: Vec<ContextEntry> = stmt.query_map(params_refs.as_slice(), |row| row_to_entry(row))?
260 .filter_map(|r| r.ok())
261 .collect();
262
263 let invalidation = self.invalidation.lock()
265 .map_err(|e| anyhow::anyhow!("Lock error: {}", e))?;
266
267 let valid_entries: Vec<ContextEntry> = entries
268 .into_iter()
269 .filter(|e| invalidation.is_valid(e))
270 .collect();
271
272 Ok(valid_entries)
273 }
274
275 pub fn keys(&self, namespace: Namespace) -> Result<Vec<String>> {
277 let conn = self.conn.lock()
278 .map_err(|e| anyhow::anyhow!("Lock error: {}", e))?;
279
280 let mut stmt = conn.prepare(
281 "SELECT key FROM context WHERE namespace = ? ORDER BY key"
282 )?;
283
284 let keys: Vec<String> = stmt.query_map([namespace.prefix()], |row| row.get(0))?
285 .filter_map(|r| r.ok())
286 .collect();
287
288 Ok(keys)
289 }
290
291 pub fn get_file_context(&self, path: &str) -> Result<Option<FileContext>> {
295 let key = format!("file:{}", path);
296 if let Some(entry) = self.get(&key)? {
297 let ctx: FileContext = serde_json::from_value(entry.value)?;
298 Ok(Some(ctx))
299 } else {
300 Ok(None)
301 }
302 }
303
304 pub fn set_file_context(&self, path: &str, ctx: &FileContext) -> Result<()> {
306 let key = format!("file:{}", path);
307 let value = serde_json::to_value(ctx)?;
308
309 let mtime = self.invalidation.lock()
311 .map_err(|e| anyhow::anyhow!("Lock error: {}", e))?
312 .get_mtime(path);
313
314 let mut entry = ContextEntry::new(&key, value)
315 .with_metadata("type", "file_context");
316
317 entry.file_path = Some(path.to_string());
318 entry.file_mtime = mtime;
319
320 if let Some(commit) = self.invalidation.lock()
322 .map_err(|e| anyhow::anyhow!("Lock error: {}", e))?
323 .head_commit()
324 {
325 entry.git_commit = Some(commit.to_string());
326 }
327
328 self.set(entry)
329 }
330
331 pub fn get_file_attr(&self, path: &str, attr: &str) -> Result<Option<Value>> {
333 let key = format!("file:{}:{}", path, attr);
334 if let Some(entry) = self.get(&key)? {
335 Ok(Some(entry.value))
336 } else {
337 Ok(None)
338 }
339 }
340
341 pub fn set_file_attr(&self, path: &str, attr: &str, value: Value) -> Result<()> {
343 let key = format!("file:{}:{}", path, attr);
344
345 let mtime = self.invalidation.lock()
346 .map_err(|e| anyhow::anyhow!("Lock error: {}", e))?
347 .get_mtime(path);
348
349 let mut entry = ContextEntry::new(&key, value);
350 entry.file_path = Some(path.to_string());
351 entry.file_mtime = mtime;
352
353 self.set(entry)
354 }
355
356 pub fn get_symbol(&self, name: &str) -> Result<Option<SymbolInfo>> {
360 let key = format!("symbol:{}", name);
361 if let Some(entry) = self.get(&key)? {
362 let info: SymbolInfo = serde_json::from_value(entry.value)?;
363 Ok(Some(info))
364 } else {
365 Ok(None)
366 }
367 }
368
369 pub fn set_symbol(&self, info: &SymbolInfo, file_path: Option<&str>) -> Result<()> {
371 let key = format!("symbol:{}", info.name);
372 let value = serde_json::to_value(info)?;
373
374 let mut entry = ContextEntry::new(&key, value);
375
376 if let Some(path) = file_path {
377 entry.file_path = Some(path.to_string());
378 entry.file_mtime = self.invalidation.lock()
379 .map_err(|e| anyhow::anyhow!("Lock error: {}", e))?
380 .get_mtime(path);
381 }
382
383 self.set(entry)
384 }
385
386 pub fn find_symbols(&self, prefix: &str) -> Result<Vec<SymbolInfo>> {
388 let query = ContextQuery::new()
389 .namespace(Namespace::Symbol)
390 .prefix(&format!("symbol:{}", prefix));
391
392 let entries = self.list(&query)?;
393 let symbols: Vec<SymbolInfo> = entries
394 .into_iter()
395 .filter_map(|e| serde_json::from_value(e.value).ok())
396 .collect();
397
398 Ok(symbols)
399 }
400
401 pub fn get_project_context(&self) -> Result<Option<ProjectContext>> {
405 let key = "project:info";
406 if let Some(entry) = self.get(key)? {
407 let ctx: ProjectContext = serde_json::from_value(entry.value)?;
408 Ok(Some(ctx))
409 } else {
410 Ok(None)
411 }
412 }
413
414 pub fn set_project_context(&self, ctx: &ProjectContext) -> Result<()> {
416 let key = "project:info";
417 let value = serde_json::to_value(ctx)?;
418
419 let mut entry = ContextEntry::new(key, value);
420
421 if let Some(commit) = self.invalidation.lock()
423 .map_err(|e| anyhow::anyhow!("Lock error: {}", e))?
424 .head_commit()
425 {
426 entry.git_commit = Some(commit.to_string());
427 }
428
429 self.set(entry)
430 }
431
432 pub fn get_project_attr(&self, attr: &str) -> Result<Option<Value>> {
434 let key = format!("project:{}", attr);
435 if let Some(entry) = self.get(&key)? {
436 Ok(Some(entry.value))
437 } else {
438 Ok(None)
439 }
440 }
441
442 pub fn set_project_attr(&self, attr: &str, value: Value) -> Result<()> {
444 let key = format!("project:{}", attr);
445 let entry = ContextEntry::new(&key, value);
446 self.set(entry)
447 }
448
449 pub fn get_session(&self, session_id: &str) -> Result<Option<SessionContext>> {
453 let key = format!("session:{}", session_id);
454 if let Some(entry) = self.get(&key)? {
455 let ctx: SessionContext = serde_json::from_value(entry.value)?;
456 Ok(Some(ctx))
457 } else {
458 Ok(None)
459 }
460 }
461
462 pub fn set_session(&self, ctx: &SessionContext) -> Result<()> {
464 let key = format!("session:{}", ctx.session_id);
465 let value = serde_json::to_value(ctx)?;
466 let entry = ContextEntry::new(&key, value);
467 self.set(entry)
468 }
469
470 pub fn update_working_files(&self, session_id: &str, files: Vec<String>) -> Result<()> {
472 if let Some(mut ctx) = self.get_session(session_id)? {
473 ctx.working_files = files;
474 ctx.last_activity = Utc::now();
475 self.set_session(&ctx)
476 } else {
477 let ctx = SessionContext {
478 session_id: session_id.to_string(),
479 working_files: files,
480 started_at: Utc::now(),
481 last_activity: Utc::now(),
482 ..Default::default()
483 };
484 self.set_session(&ctx)
485 }
486 }
487
488 pub fn add_decision(
490 &self,
491 session_id: &str,
492 decision: &str,
493 rationale: Option<&str>,
494 context: Vec<String>,
495 ) -> Result<()> {
496 let mut ctx = self.get_session(session_id)?
497 .unwrap_or_else(|| SessionContext {
498 session_id: session_id.to_string(),
499 started_at: Utc::now(),
500 last_activity: Utc::now(),
501 ..Default::default()
502 });
503
504 ctx.decisions.push(Decision {
505 decision: decision.to_string(),
506 rationale: rationale.map(|s| s.to_string()),
507 timestamp: Utc::now(),
508 context,
509 });
510 ctx.last_activity = Utc::now();
511
512 self.set_session(&ctx)
513 }
514
515 pub fn get_batch(&self, keys: &[String]) -> Result<HashMap<String, ContextEntry>> {
519 let mut results = HashMap::new();
520 for key in keys {
521 if let Some(entry) = self.get(key)? {
522 results.insert(key.clone(), entry);
523 }
524 }
525 Ok(results)
526 }
527
528 pub fn set_batch(&self, entries: Vec<ContextEntry>) -> Result<()> {
530 let conn = self.conn.lock()
531 .map_err(|e| anyhow::anyhow!("Lock error: {}", e))?;
532
533 let tx = conn.unchecked_transaction()?;
534
535 for entry in entries {
536 let (namespace, _) = Namespace::from_key(&entry.key);
537 let metadata_json = serde_json::to_string(&entry.metadata)?;
538
539 tx.execute(
540 "INSERT OR REPLACE INTO context
541 (key, value, namespace, created_at, updated_at, expires_at,
542 git_commit, file_path, file_mtime, metadata)
543 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
544 params![
545 entry.key,
546 entry.value.to_string(),
547 namespace.prefix(),
548 entry.created_at.to_rfc3339(),
549 entry.updated_at.to_rfc3339(),
550 entry.expires_at.map(|t| t.to_rfc3339()),
551 entry.git_commit,
552 entry.file_path,
553 entry.file_mtime,
554 metadata_json,
555 ],
556 )?;
557 }
558
559 tx.commit()?;
560 Ok(())
561 }
562
563 pub fn cleanup_expired(&self) -> Result<usize> {
567 let conn = self.conn.lock()
568 .map_err(|e| anyhow::anyhow!("Lock error: {}", e))?;
569
570 let now = Utc::now().to_rfc3339();
571 let rows = conn.execute(
572 "DELETE FROM context WHERE expires_at IS NOT NULL AND expires_at < ?",
573 [&now],
574 )?;
575
576 Ok(rows)
577 }
578
579 pub fn cleanup_invalid(&self) -> Result<usize> {
581 let entries = self.list(&ContextQuery::new().include_expired())?;
582
583 let invalidation = self.invalidation.lock()
584 .map_err(|e| anyhow::anyhow!("Lock error: {}", e))?;
585
586 let invalid_keys: Vec<String> = entries
587 .iter()
588 .filter(|e| !invalidation.is_valid(e))
589 .map(|e| e.key.clone())
590 .collect();
591
592 drop(invalidation);
593
594 let mut deleted = 0;
595 for key in invalid_keys {
596 if self.delete(&key)? {
597 deleted += 1;
598 }
599 }
600
601 Ok(deleted)
602 }
603
604 pub fn stats(&self) -> Result<ContextStats> {
606 let conn = self.conn.lock()
607 .map_err(|e| anyhow::anyhow!("Lock error: {}", e))?;
608
609 let total: usize = conn.query_row(
610 "SELECT COUNT(*) FROM context",
611 [],
612 |row| row.get(0),
613 )?;
614
615 let by_namespace: HashMap<String, usize> = {
616 let mut stmt = conn.prepare(
617 "SELECT namespace, COUNT(*) FROM context GROUP BY namespace"
618 )?;
619 let rows = stmt.query_map([], |row| {
620 Ok((row.get::<_, String>(0)?, row.get::<_, usize>(1)?))
621 })?;
622 rows.filter_map(|r| r.ok()).collect()
623 };
624
625 let expired: usize = conn.query_row(
626 "SELECT COUNT(*) FROM context WHERE expires_at IS NOT NULL AND expires_at < ?",
627 [Utc::now().to_rfc3339()],
628 |row| row.get(0),
629 )?;
630
631 Ok(ContextStats {
632 total_entries: total,
633 by_namespace,
634 expired_entries: expired,
635 })
636 }
637
638 pub fn clear(&self) -> Result<()> {
640 let conn = self.conn.lock()
641 .map_err(|e| anyhow::anyhow!("Lock error: {}", e))?;
642 conn.execute("DELETE FROM context", [])?;
643 Ok(())
644 }
645
646 pub fn clear_all(&self) -> Result<usize> {
648 let conn = self.conn.lock()
649 .map_err(|e| anyhow::anyhow!("Lock error: {}", e))?;
650 let count: usize = conn.query_row(
651 "SELECT COUNT(*) FROM context",
652 [],
653 |row| row.get(0),
654 )?;
655 conn.execute("DELETE FROM context", [])?;
656 Ok(count)
657 }
658
659 pub fn clear_namespace(&self, namespace: Namespace) -> Result<usize> {
661 let conn = self.conn.lock()
662 .map_err(|e| anyhow::anyhow!("Lock error: {}", e))?;
663 let count: usize = conn.query_row(
664 "SELECT COUNT(*) FROM context WHERE namespace = ?",
665 [namespace.prefix()],
666 |row| row.get(0),
667 )?;
668 conn.execute(
669 "DELETE FROM context WHERE namespace = ?",
670 [namespace.prefix()],
671 )?;
672 Ok(count)
673 }
674
675 pub fn get_file_mtime(&self, path: &str) -> Option<i64> {
677 let invalidation = self.invalidation.lock().ok()?;
678 invalidation.get_mtime(path)
679 }
680
681 pub fn refresh_invalidation(&self) -> Result<()> {
683 self.refresh_git_state()
684 }
685
686 pub fn list_simple(&self, namespace: Option<Namespace>, prefix: Option<&str>) -> Result<Vec<ContextEntry>> {
688 let mut query = ContextQuery::new();
689 if let Some(ns) = namespace {
690 query = query.namespace(ns);
691 }
692 if let Some(p) = prefix {
693 query = query.prefix(p);
694 }
695 self.list(&query)
696 }
697}
698
699#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
701pub struct ContextStats {
702 pub total_entries: usize,
703 pub by_namespace: HashMap<String, usize>,
704 pub expired_entries: usize,
705}
706
707fn row_to_entry(row: &rusqlite::Row) -> rusqlite::Result<ContextEntry> {
709 let value_str: String = row.get(1)?;
710 let metadata_str: String = row.get(9)?;
711
712 Ok(ContextEntry {
713 key: row.get(0)?,
714 value: serde_json::from_str(&value_str).unwrap_or(Value::Null),
715 created_at: parse_datetime(&row.get::<_, String>(3)?),
716 updated_at: parse_datetime(&row.get::<_, String>(4)?),
717 expires_at: row.get::<_, Option<String>>(5)?.map(|s| parse_datetime(&s)),
718 git_commit: row.get(6)?,
719 file_path: row.get(7)?,
720 file_mtime: row.get(8)?,
721 metadata: serde_json::from_str(&metadata_str).unwrap_or_default(),
722 })
723}
724
725fn parse_datetime(s: &str) -> DateTime<Utc> {
726 DateTime::parse_from_rfc3339(s)
727 .map(|dt| dt.with_timezone(&Utc))
728 .unwrap_or_else(|_| Utc::now())
729}
730
731#[cfg(test)]
732mod tests {
733 use super::*;
734 use serde_json::json;
735
736 #[test]
737 fn test_basic_kv_operations() {
738 let store = ContextStore::in_memory().unwrap();
739
740 store.set_value("test:key1", json!({"data": "value1"})).unwrap();
742 let entry = store.get("test:key1").unwrap().unwrap();
743 assert_eq!(entry.value, json!({"data": "value1"}));
744
745 assert!(store.delete("test:key1").unwrap());
747 assert!(store.get("test:key1").unwrap().is_none());
748 }
749
750 #[test]
751 fn test_file_context() {
752 let store = ContextStore::in_memory().unwrap();
753
754 let ctx = FileContext {
755 path: "src/main.rs".to_string(),
756 summary: Some("Main entry point".to_string()),
757 language: Some("rust".to_string()),
758 ..Default::default()
759 };
760
761 store.set_file_context("src/main.rs", &ctx).unwrap();
762
763 let retrieved = store.get_file_context("src/main.rs").unwrap().unwrap();
764 assert_eq!(retrieved.summary, Some("Main entry point".to_string()));
765 }
766
767 #[test]
768 fn test_session_context() {
769 let store = ContextStore::in_memory().unwrap();
770
771 store.update_working_files("session-1", vec!["file1.rs".to_string()]).unwrap();
773
774 store.add_decision(
776 "session-1",
777 "Use async/await for IO",
778 Some("Better concurrency"),
779 vec!["src/io.rs".to_string()],
780 ).unwrap();
781
782 let session = store.get_session("session-1").unwrap().unwrap();
783 assert_eq!(session.working_files, vec!["file1.rs"]);
784 assert_eq!(session.decisions.len(), 1);
785 assert_eq!(session.decisions[0].decision, "Use async/await for IO");
786 }
787
788 #[test]
789 fn test_namespace_listing() {
790 let store = ContextStore::in_memory().unwrap();
791
792 store.set_value("file:a.rs", json!({})).unwrap();
793 store.set_value("file:b.rs", json!({})).unwrap();
794 store.set_value("project:info", json!({})).unwrap();
795
796 let file_keys = store.keys(Namespace::File).unwrap();
797 assert_eq!(file_keys.len(), 2);
798
799 let project_keys = store.keys(Namespace::Project).unwrap();
800 assert_eq!(project_keys.len(), 1);
801 }
802
803 #[test]
804 fn test_ttl_expiration() {
805 let store = ContextStore::in_memory().unwrap();
806
807 let mut entry = ContextEntry::new("test:expired", json!({}));
809 entry.expires_at = Some(Utc::now() - chrono::Duration::hours(1));
810 store.set(entry).unwrap();
811
812 assert!(store.get("test:expired").unwrap().is_none());
814 }
815}