use super::*;
pub(super) fn scan_unbound_memories(
conn: &Connection,
namespace: &str,
limit: Option<usize>,
name_filter: &[String],
) -> Result<Vec<(i64, String, String)>, AppError> {
let limit_clause = limit.map(|n| format!("LIMIT {n}")).unwrap_or_default();
if name_filter.is_empty() {
let sql = format!(
"SELECT m.id, m.name, m.body
FROM memories m
WHERE m.namespace = ?1
AND m.deleted_at IS NULL
AND NOT EXISTS (
SELECT 1 FROM memory_entities me WHERE me.memory_id = m.id
)
ORDER BY m.id
{limit_clause}"
);
let mut stmt = conn.prepare(&sql)?;
let rows = stmt
.query_map(rusqlite::params![namespace], |r| {
Ok((
r.get::<_, i64>(0)?,
r.get::<_, String>(1)?,
r.get::<_, String>(2)?,
))
})?
.collect::<Result<Vec<_>, _>>()?;
Ok(rows)
} else {
let placeholders: Vec<String> = (2..=name_filter.len() + 1)
.map(|i| format!("?{i}"))
.collect();
let in_clause = placeholders.join(", ");
let sql = format!(
"SELECT m.id, m.name, m.body
FROM memories m
WHERE m.namespace = ?1
AND m.deleted_at IS NULL
AND m.name IN ({in_clause})
AND NOT EXISTS (
SELECT 1 FROM memory_entities me WHERE me.memory_id = m.id
)
ORDER BY m.id
{limit_clause}"
);
let mut params_vec: Vec<&dyn rusqlite::ToSql> = Vec::with_capacity(1 + name_filter.len());
params_vec.push(&namespace);
for n in name_filter {
params_vec.push(n);
}
let mut stmt = conn.prepare(&sql)?;
let rows = stmt
.query_map(
rusqlite::params_from_iter(params_vec.iter().copied()),
|r| {
Ok((
r.get::<_, i64>(0)?,
r.get::<_, String>(1)?,
r.get::<_, String>(2)?,
))
},
)?
.collect::<Result<Vec<_>, _>>()?;
Ok(rows)
}
}
pub(super) fn scan_bound_memories_for_augment(
conn: &Connection,
namespace: &str,
limit: Option<usize>,
name_filter: &[String],
) -> Result<Vec<String>, AppError> {
if name_filter.is_empty() {
return Err(AppError::Validation(
"augment-bindings requires an explicit subset: pass --names or \
--names-file (it refuses to re-scan the whole namespace)"
.into(),
));
}
let limit_clause = limit.map(|n| format!("LIMIT {n}")).unwrap_or_default();
let placeholders: Vec<String> = (2..=name_filter.len() + 1)
.map(|i| format!("?{i}"))
.collect();
let in_clause = placeholders.join(", ");
let sql = format!(
"SELECT m.name
FROM memories m
WHERE m.namespace = ?1
AND m.deleted_at IS NULL
AND m.name IN ({in_clause})
AND EXISTS (
SELECT 1 FROM memory_entities me WHERE me.memory_id = m.id
)
ORDER BY m.id
{limit_clause}"
);
let mut params_vec: Vec<&dyn rusqlite::ToSql> = Vec::with_capacity(1 + name_filter.len());
params_vec.push(&namespace);
for n in name_filter {
params_vec.push(n);
}
let mut stmt = conn.prepare(&sql)?;
let rows = stmt
.query_map(
rusqlite::params_from_iter(params_vec.iter().copied()),
|r| r.get::<_, String>(0),
)?
.collect::<Result<Vec<_>, _>>()?;
Ok(rows)
}
pub(super) fn read_names_file(path: &Path) -> Result<Vec<String>, AppError> {
let content = std::fs::read_to_string(path).map_err(|e| {
AppError::Validation(format!("failed to read names file {}: {e}", path.display()))
})?;
let mut seen = std::collections::HashSet::new();
let mut out = Vec::new();
for line in content.lines() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
if seen.insert(trimmed.to_string()) {
out.push(trimmed.to_string());
}
}
Ok(out)
}
pub(super) fn resolve_name_filter(args: &EnrichArgs) -> Result<Vec<String>, AppError> {
let mut combined: Vec<String> = args.names.clone();
if let Some(p) = &args.names_file {
let from_file = read_names_file(p)?;
for n in from_file {
if !combined.contains(&n) {
combined.push(n);
}
}
}
Ok(combined)
}
pub(super) fn scan_entities_without_description(
conn: &Connection,
namespace: &str,
limit: Option<usize>,
name_filter: &[String],
) -> Result<Vec<(i64, String, String)>, AppError> {
let limit_clause = limit.map(|n| format!("LIMIT {n}")).unwrap_or_default();
if name_filter.is_empty() {
let sql = format!(
"SELECT id, name, type
FROM entities
WHERE namespace = ?1
AND (description IS NULL OR description = '')
ORDER BY id
{limit_clause}"
);
let mut stmt = conn.prepare(&sql)?;
let rows = stmt
.query_map(rusqlite::params![namespace], |r| {
Ok((
r.get::<_, i64>(0)?,
r.get::<_, String>(1)?,
r.get::<_, String>(2)?,
))
})?
.collect::<Result<Vec<_>, _>>()?;
Ok(rows)
} else {
let placeholders: Vec<String> = (2..=name_filter.len() + 1)
.map(|i| format!("?{i}"))
.collect();
let in_clause = placeholders.join(", ");
let sql = format!(
"SELECT id, name, type
FROM entities
WHERE namespace = ?1
AND name IN ({in_clause})
AND (description IS NULL OR description = '')
ORDER BY id
{limit_clause}"
);
let mut params_vec: Vec<&dyn rusqlite::ToSql> = Vec::with_capacity(1 + name_filter.len());
params_vec.push(&namespace);
for n in name_filter {
params_vec.push(n);
}
let mut stmt = conn.prepare(&sql)?;
let rows = stmt
.query_map(
rusqlite::params_from_iter(params_vec.iter().copied()),
|r| {
Ok((
r.get::<_, i64>(0)?,
r.get::<_, String>(1)?,
r.get::<_, String>(2)?,
))
},
)?
.collect::<Result<Vec<_>, _>>()?;
Ok(rows)
}
}
pub(super) fn scan_short_body_memories(
conn: &Connection,
namespace: &str,
min_chars: usize,
limit: Option<usize>,
name_filter: &[String],
) -> Result<Vec<(i64, String, String)>, AppError> {
let limit_clause = limit.map(|n| format!("LIMIT {n}")).unwrap_or_default();
if name_filter.is_empty() {
let sql = format!(
"SELECT m.id, m.name, m.body
FROM memories m
WHERE m.namespace = ?1
AND m.deleted_at IS NULL
AND LENGTH(COALESCE(m.body,'')) < ?2
ORDER BY m.id
{limit_clause}"
);
let mut stmt = conn.prepare(&sql)?;
let rows = stmt
.query_map(rusqlite::params![namespace, min_chars as i64], |r| {
Ok((
r.get::<_, i64>(0)?,
r.get::<_, String>(1)?,
r.get::<_, String>(2)?,
))
})?
.collect::<Result<Vec<_>, _>>()?;
Ok(rows)
} else {
let placeholders: Vec<String> = (3..=name_filter.len() + 2)
.map(|i| format!("?{i}"))
.collect();
let in_clause = placeholders.join(", ");
let sql = format!(
"SELECT m.id, m.name, m.body
FROM memories m
WHERE m.namespace = ?1
AND m.deleted_at IS NULL
AND m.name IN ({in_clause})
AND LENGTH(COALESCE(m.body,'')) < ?2
ORDER BY m.id
{limit_clause}"
);
let mut params_vec: Vec<&dyn rusqlite::ToSql> = Vec::with_capacity(2 + name_filter.len());
let min_chars_i64 = min_chars as i64;
params_vec.push(&namespace);
params_vec.push(&min_chars_i64);
for n in name_filter {
params_vec.push(n);
}
let mut stmt = conn.prepare(&sql)?;
let rows = stmt
.query_map(
rusqlite::params_from_iter(params_vec.iter().copied()),
|r| {
Ok((
r.get::<_, i64>(0)?,
r.get::<_, String>(1)?,
r.get::<_, String>(2)?,
))
},
)?
.collect::<Result<Vec<_>, _>>()?;
Ok(rows)
}
}
pub(super) fn scan_memories_without_embeddings(
conn: &Connection,
namespace: &str,
limit: Option<usize>,
name_filter: &[String],
) -> Result<Vec<(i64, String, String)>, AppError> {
let limit_clause = limit.map(|n| format!("LIMIT {n}")).unwrap_or_default();
if name_filter.is_empty() {
let sql = format!(
"SELECT m.id, m.name, COALESCE(m.body,'')
FROM memories m
LEFT JOIN memory_embeddings me ON me.memory_id = m.id
WHERE m.namespace = ?1
AND m.deleted_at IS NULL
AND me.memory_id IS NULL
ORDER BY m.id
{limit_clause}"
);
let mut stmt = conn.prepare(&sql)?;
let rows = stmt
.query_map(rusqlite::params![namespace], |r| {
Ok((
r.get::<_, i64>(0)?,
r.get::<_, String>(1)?,
r.get::<_, String>(2)?,
))
})?
.collect::<Result<Vec<_>, _>>()?;
Ok(rows)
} else {
let placeholders: Vec<String> = (2..=name_filter.len() + 1)
.map(|i| format!("?{i}"))
.collect();
let in_clause = placeholders.join(", ");
let sql = format!(
"SELECT m.id, m.name, COALESCE(m.body,'')
FROM memories m
LEFT JOIN memory_embeddings me ON me.memory_id = m.id
WHERE m.namespace = ?1
AND m.deleted_at IS NULL
AND m.name IN ({in_clause})
AND me.memory_id IS NULL
ORDER BY m.id
{limit_clause}"
);
let mut params_vec: Vec<&dyn rusqlite::ToSql> = Vec::with_capacity(1 + name_filter.len());
params_vec.push(&namespace);
for n in name_filter {
params_vec.push(n);
}
let mut stmt = conn.prepare(&sql)?;
let rows = stmt
.query_map(
rusqlite::params_from_iter(params_vec.iter().copied()),
|r| {
Ok((
r.get::<_, i64>(0)?,
r.get::<_, String>(1)?,
r.get::<_, String>(2)?,
))
},
)?
.collect::<Result<Vec<_>, _>>()?;
Ok(rows)
}
}
#[allow(clippy::type_complexity)]
pub(super) fn scan_weight_candidates(
conn: &Connection,
namespace: &str,
limit: Option<usize>,
) -> Result<Vec<(i64, String, String, String, f64)>, AppError> {
let limit_clause = limit.map(|n| format!("LIMIT {n}")).unwrap_or_default();
let sql = format!(
"SELECT r.id, e1.name, e2.name, r.relation, r.weight \
FROM relationships r \
JOIN entities e1 ON e1.id = r.source_id \
JOIN entities e2 ON e2.id = r.target_id \
WHERE r.weight >= 0.7 AND e1.namespace = ?1 \
ORDER BY r.weight DESC {limit_clause}"
);
let mut stmt = conn.prepare(&sql)?;
let rows = stmt
.query_map(rusqlite::params![namespace], |r| {
Ok((
r.get::<_, i64>(0)?,
r.get::<_, String>(1)?,
r.get::<_, String>(2)?,
r.get::<_, String>(3)?,
r.get::<_, f64>(4)?,
))
})?
.collect::<Result<Vec<_>, _>>()?;
Ok(rows)
}
pub(super) fn scan_generic_relations(
conn: &Connection,
namespace: &str,
limit: Option<usize>,
) -> Result<Vec<(i64, String, String, String)>, AppError> {
let limit_clause = limit.map(|n| format!("LIMIT {n}")).unwrap_or_default();
let sql = format!(
"SELECT r.id, e1.name, e2.name, r.relation \
FROM relationships r \
JOIN entities e1 ON e1.id = r.source_id \
JOIN entities e2 ON e2.id = r.target_id \
WHERE r.relation = 'applies_to' AND e1.namespace = ?1 \
ORDER BY r.id {limit_clause}"
);
let mut stmt = conn.prepare(&sql)?;
let rows = stmt
.query_map(rusqlite::params![namespace], |r| {
Ok((
r.get::<_, i64>(0)?,
r.get::<_, String>(1)?,
r.get::<_, String>(2)?,
r.get::<_, String>(3)?,
))
})?
.collect::<Result<Vec<_>, _>>()?;
Ok(rows)
}
pub(super) fn scan_operation(
conn: &Connection,
namespace: &str,
args: &EnrichArgs,
) -> Result<Vec<String>, AppError> {
let name_filter = resolve_name_filter(args)?;
match args.operation() {
EnrichOperation::MemoryBindings => {
let rows = scan_unbound_memories(conn, namespace, args.limit, &name_filter)?;
Ok(rows.into_iter().map(|(_, name, _)| name).collect())
}
EnrichOperation::AugmentBindings => {
scan_bound_memories_for_augment(conn, namespace, args.limit, &name_filter)
}
EnrichOperation::EntityDescriptions => {
let rows =
scan_entities_without_description(conn, namespace, args.limit, &name_filter)?;
Ok(rows.into_iter().map(|(_, name, _)| name).collect())
}
EnrichOperation::BodyEnrich => {
let rows = scan_short_body_memories(
conn,
namespace,
args.min_output_chars,
args.limit,
&name_filter,
)?;
Ok(rows.into_iter().map(|(_, name, _)| name).collect())
}
EnrichOperation::ReEmbed => {
let rows = scan_memories_without_embeddings(conn, namespace, args.limit, &name_filter)?;
Ok(rows.into_iter().map(|(_, name, _)| name).collect())
}
EnrichOperation::WeightCalibrate => {
let rows = scan_weight_candidates(conn, namespace, args.limit)?;
Ok(rows
.into_iter()
.map(|(id, _, _, _, _)| id.to_string())
.collect())
}
EnrichOperation::RelationReclassify => {
let rows = scan_generic_relations(conn, namespace, args.limit)?;
Ok(rows
.into_iter()
.map(|(id, _, _, _)| id.to_string())
.collect())
}
EnrichOperation::EntityConnect | EnrichOperation::CrossDomainBridges => {
let pairs = scan_isolated_entity_pairs(conn, namespace, args.limit)?;
Ok(pairs.into_iter().map(|(_, name, _, _)| name).collect())
}
EnrichOperation::EntityTypeValidate => {
let rows = scan_entities_for_type_validation(conn, namespace, args.limit)?;
Ok(rows.into_iter().map(|(_, name, _)| name).collect())
}
EnrichOperation::DescriptionEnrich => {
let rows = scan_generic_descriptions(conn, namespace, args.limit)?;
Ok(rows.into_iter().map(|(_, name, _)| name).collect())
}
EnrichOperation::DomainClassify
| EnrichOperation::GraphAudit
| EnrichOperation::DeepResearchSynth
| EnrichOperation::BodyExtract => {
let limit_clause = args.limit.map(|n| format!("LIMIT {n}")).unwrap_or_default();
let sql = format!(
"SELECT name FROM memories WHERE namespace=?1 AND deleted_at IS NULL ORDER BY id {limit_clause}"
);
let mut stmt = conn.prepare(&sql)?;
let mut names = stmt
.query_map(rusqlite::params![namespace], |r| r.get::<_, String>(0))?
.collect::<Result<Vec<_>, _>>()?;
if !name_filter.is_empty() {
names.retain(|n| name_filter.iter().any(|f| f == n));
}
Ok(names)
}
}
}
#[allow(clippy::type_complexity)]
pub(super) fn scan_isolated_entity_pairs(
conn: &Connection,
namespace: &str,
limit: Option<usize>,
) -> Result<Vec<(i64, String, i64, String)>, AppError> {
let limit_val = limit.unwrap_or(50) as i64;
let mut stmt = conn.prepare_cached(
"SELECT e1.id, e1.name, e2.id, e2.name FROM entities e1, entities e2 \
WHERE e1.namespace = ?1 AND e2.namespace = ?1 AND e1.id < e2.id \
AND NOT EXISTS (SELECT 1 FROM relationships r WHERE \
(r.source_id = e1.id AND r.target_id = e2.id) OR \
(r.source_id = e2.id AND r.target_id = e1.id)) \
LIMIT ?2",
)?;
let rows = stmt
.query_map(rusqlite::params![namespace, limit_val], |r| {
Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?))
})?
.collect::<Result<Vec<_>, _>>()?;
Ok(rows)
}
pub(super) fn scan_entities_for_type_validation(
conn: &Connection,
namespace: &str,
limit: Option<usize>,
) -> Result<Vec<(i64, String, String)>, AppError> {
let limit_clause = limit.map(|n| format!("LIMIT {n}")).unwrap_or_default();
let sql = format!(
"SELECT id, name, type FROM entities WHERE namespace = ?1 ORDER BY id {limit_clause}"
);
let mut stmt = conn.prepare(&sql)?;
let rows = stmt
.query_map(rusqlite::params![namespace], |r| {
Ok((r.get(0)?, r.get(1)?, r.get(2)?))
})?
.collect::<Result<Vec<_>, _>>()?;
Ok(rows)
}
pub(super) fn scan_generic_descriptions(
conn: &Connection,
namespace: &str,
limit: Option<usize>,
) -> Result<Vec<(i64, String, String)>, AppError> {
let limit_clause = limit.map(|n| format!("LIMIT {n}")).unwrap_or_default();
let sql = format!(
"SELECT id, name, description FROM memories WHERE namespace = ?1 AND deleted_at IS NULL \
AND (description LIKE '%ingested%' OR description LIKE '%imported%' OR description LIKE '%added%' OR length(description) < 30) \
ORDER BY id {limit_clause}"
);
let mut stmt = conn.prepare(&sql)?;
let rows = stmt
.query_map(rusqlite::params![namespace], |r| {
Ok((r.get(0)?, r.get(1)?, r.get(2)?))
})?
.collect::<Result<Vec<_>, _>>()?;
Ok(rows)
}
#[cfg(test)]
mod tests {
use super::*;
use rusqlite::Connection;
fn open_test_db() -> Connection {
let conn = Connection::open_in_memory().expect("in-memory db");
conn.execute_batch(
"CREATE TABLE memories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
namespace TEXT NOT NULL DEFAULT 'global',
name TEXT NOT NULL,
type TEXT NOT NULL DEFAULT 'note',
description TEXT NOT NULL DEFAULT '',
body TEXT NOT NULL DEFAULT '',
body_hash TEXT NOT NULL DEFAULT '',
session_id TEXT,
source TEXT NOT NULL DEFAULT 'agent',
metadata TEXT NOT NULL DEFAULT '{}',
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
deleted_at INTEGER,
UNIQUE(namespace, name)
);
CREATE TABLE entities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
namespace TEXT NOT NULL DEFAULT 'global',
name TEXT NOT NULL,
type TEXT NOT NULL DEFAULT 'concept',
description TEXT,
degree INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
UNIQUE(namespace, name)
);
CREATE TABLE memory_entities (
memory_id INTEGER NOT NULL,
entity_id INTEGER NOT NULL,
PRIMARY KEY (memory_id, entity_id)
);
CREATE TABLE relationships (
id INTEGER PRIMARY KEY AUTOINCREMENT,
namespace TEXT NOT NULL DEFAULT 'global',
source_id INTEGER NOT NULL,
target_id INTEGER NOT NULL,
relation TEXT NOT NULL,
weight REAL NOT NULL DEFAULT 0.5,
description TEXT,
UNIQUE(source_id, target_id, relation)
);
CREATE TABLE memory_embeddings (
memory_id INTEGER PRIMARY KEY,
namespace TEXT NOT NULL,
embedding BLOB NOT NULL,
source TEXT NOT NULL,
model TEXT NOT NULL DEFAULT '',
dim INTEGER NOT NULL DEFAULT 384,
created_at INTEGER NOT NULL DEFAULT (unixepoch())
);",
)
.expect("schema creation must succeed");
conn
}
#[test]
fn scan_unbound_memories_finds_memories_without_bindings() {
let conn = open_test_db();
conn.execute(
"INSERT INTO memories (namespace, name, body) VALUES ('global', 'test-mem', 'some body content')",
[],
)
.unwrap();
let results = scan_unbound_memories(&conn, "global", None, &[]).unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].1, "test-mem");
}
#[test]
fn scan_unbound_memories_excludes_bound_memories() {
let conn = open_test_db();
conn.execute(
"INSERT INTO memories (namespace, name, body) VALUES ('global', 'bound-mem', 'body')",
[],
)
.unwrap();
let mem_id: i64 = conn
.query_row("SELECT id FROM memories WHERE name='bound-mem'", [], |r| {
r.get(0)
})
.unwrap();
conn.execute(
"INSERT INTO entities (namespace, name) VALUES ('global', 'some-entity')",
[],
)
.unwrap();
let ent_id: i64 = conn
.query_row(
"SELECT id FROM entities WHERE name='some-entity'",
[],
|r| r.get(0),
)
.unwrap();
conn.execute(
"INSERT INTO memory_entities (memory_id, entity_id) VALUES (?1, ?2)",
rusqlite::params![mem_id, ent_id],
)
.unwrap();
let results = scan_unbound_memories(&conn, "global", None, &[]).unwrap();
assert!(results.is_empty(), "bound memory must not appear in scan");
}
#[test]
fn scan_entities_without_description_finds_null_description() {
let conn = open_test_db();
conn.execute(
"INSERT INTO entities (namespace, name, type, description) VALUES ('global', 'my-tool', 'tool', NULL)",
[],
)
.unwrap();
let results = scan_entities_without_description(&conn, "global", None, &[]).unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].1, "my-tool");
}
#[test]
fn scan_entities_without_description_excludes_entities_with_description() {
let conn = open_test_db();
conn.execute(
"INSERT INTO entities (namespace, name, type, description) VALUES ('global', 'described-tool', 'tool', 'Has a description already')",
[],
)
.unwrap();
let results = scan_entities_without_description(&conn, "global", None, &[]).unwrap();
assert!(
results.is_empty(),
"entity with description must not appear"
);
}
#[test]
fn scan_short_body_memories_finds_short_bodies() {
let conn = open_test_db();
conn.execute(
"INSERT INTO memories (namespace, name, body) VALUES ('global', 'short-mem', 'hi')",
[],
)
.unwrap();
let results = scan_short_body_memories(&conn, "global", 100, None, &[]).unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].1, "short-mem");
}
#[test]
fn scan_short_body_memories_excludes_long_bodies() {
let conn = open_test_db();
let long_body = "a".repeat(1000);
conn.execute(
"INSERT INTO memories (namespace, name, body) VALUES ('global', 'long-mem', ?1)",
rusqlite::params![long_body],
)
.unwrap();
let results = scan_short_body_memories(&conn, "global", 100, None, &[]).unwrap();
assert!(results.is_empty(), "long memory must not appear in scan");
}
#[test]
fn scan_respects_limit() {
let conn = open_test_db();
for i in 0..5 {
conn.execute(
&format!("INSERT INTO memories (namespace, name, body) VALUES ('global', 'mem-{i}', 'short')"),
[],
)
.unwrap();
}
let results = scan_short_body_memories(&conn, "global", 1000, Some(3), &[]).unwrap();
assert_eq!(results.len(), 3, "limit must be respected");
}
#[test]
fn scan_memories_without_embeddings_finds_only_missing_rows() {
let conn = open_test_db();
conn.execute(
"INSERT INTO memories (namespace, name, body) VALUES ('global', 'missing-vec', 'body one')",
[],
)
.unwrap();
conn.execute(
"INSERT INTO memories (namespace, name, body) VALUES ('global', 'has-vec', 'body two')",
[],
)
.unwrap();
let memory_id: i64 = conn
.query_row(
"SELECT id FROM memories WHERE namespace='global' AND name='has-vec'",
[],
|r| r.get(0),
)
.unwrap();
let embedding = vec![0.0_f32; crate::constants::embedding_dim()];
crate::storage::memories::upsert_vec(
&conn, memory_id, "global", "note", &embedding, "has-vec", "body two",
)
.unwrap();
let results = scan_memories_without_embeddings(&conn, "global", None, &[]).unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].1, "missing-vec");
}
#[test]
fn scan_memories_without_embeddings_respects_name_filter() {
let conn = open_test_db();
conn.execute(
"INSERT INTO memories (namespace, name, body) VALUES ('global', 'match-me', 'body one')",
[],
)
.unwrap();
conn.execute(
"INSERT INTO memories (namespace, name, body) VALUES ('global', 'skip-me', 'body two')",
[],
)
.unwrap();
let results =
scan_memories_without_embeddings(&conn, "global", None, &["match-me".to_string()])
.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].1, "match-me");
}
#[test]
fn dry_run_emits_preview_without_calling_llm() {
let conn = open_test_db();
conn.execute(
"INSERT INTO memories (namespace, name, body) VALUES ('global', 'dry-mem', 'tiny')",
[],
)
.unwrap();
let results = scan_short_body_memories(&conn, "global", 1000, None, &[]).unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].1, "dry-mem");
}
#[test]
fn scan_bound_memories_for_augment_requires_names_and_finds_bound() {
let conn = open_test_db();
conn.execute(
"INSERT INTO memories (id, namespace, name, body) VALUES (1, 'global', 'bound', 'b')",
[],
)
.unwrap();
conn.execute(
"INSERT INTO memories (id, namespace, name, body) VALUES (2, 'global', 'unbound', 'b')",
[],
)
.unwrap();
conn.execute(
"INSERT INTO entities (id, namespace, name) VALUES (10, 'global', 'e')",
[],
)
.unwrap();
conn.execute(
"INSERT INTO memory_entities (memory_id, entity_id) VALUES (1, 10)",
[],
)
.unwrap();
assert!(scan_bound_memories_for_augment(&conn, "global", None, &[]).is_err());
let names = scan_bound_memories_for_augment(
&conn,
"global",
None,
&["bound".to_string(), "unbound".to_string()],
)
.unwrap();
assert_eq!(names, vec!["bound".to_string()]);
}
}