use super::*;
use crate::memory;
use anyhow::Result;
use rusqlite::{params, Connection};
mod plan;
const STASH: &str = "/Users/lifcc/Desktop/code/AI/tool/stash";
const NOW: i64 = 1_780_000_000;
fn setup_conn() -> Connection {
let conn = Connection::open_in_memory().unwrap();
memory::types::tests_helper::setup_memory_schema(&conn);
setup_workstream_schema(&conn);
conn
}
fn setup_workstream_schema(conn: &Connection) {
conn.execute_batch(
"CREATE TABLE workstreams (
id INTEGER PRIMARY KEY,
project TEXT NOT NULL,
title TEXT NOT NULL,
description TEXT,
status TEXT NOT NULL DEFAULT 'active',
progress TEXT,
next_action TEXT,
blockers TEXT,
created_at_epoch INTEGER NOT NULL,
updated_at_epoch INTEGER NOT NULL,
completed_at_epoch INTEGER,
source_project TEXT,
target_project TEXT,
owner_scope TEXT,
owner_key TEXT,
topic_domain TEXT,
routing_confidence REAL,
routing_reason TEXT,
context_class TEXT,
expires_at_epoch INTEGER,
valid_from_epoch INTEGER,
valid_to_epoch INTEGER
);",
)
.unwrap();
}
fn seed_stash_pollution(conn: &Connection) {
let memory_rows = [
(
1001,
"Stash DnD sensors",
"Stash DnD uses pointer sensors for drag and drop.",
"architecture",
"stash-ui",
0.95,
None,
),
(
1002,
"Stash dev server",
"Stash dev server runs on Vite port 5173.",
"discovery",
"stash-dev",
0.95,
Some(NOW + 86_400),
),
(
1010,
"Codex sandbox",
"Codex CLI uses workspace-write sandbox approvals.",
"discovery",
"codex-sandbox",
0.95,
None,
),
(
1011,
"Codex approval prompts",
"Codex approval prompts require explicit confirm before destructive commands.",
"procedure",
"codex-sandbox",
0.95,
None,
),
(
1020,
"Grok API payloads",
"Grok API supports image reference payloads.",
"discovery",
"grok-api",
0.95,
None,
),
(
1021,
"Warp macOS launch",
"Warp terminal launch behavior depends on macOS app routing.",
"discovery",
"macos",
0.95,
None,
),
(
1030,
"Preference: direct UI critique",
"Prefer direct UI critique in Stash reviews.",
"preference",
"stash-ui",
0.95,
None,
),
(
1031,
"Preference: Stash UI critique",
"For Stash UI reviews, prefer direct critique and avoid decorative fluff.",
"preference",
"stash-ui",
0.95,
None,
),
(
1032,
"Preference: concise critique",
"Prefer concise UI critique when reviewing Stash.",
"preference",
"stash-ui",
0.95,
None,
),
(
1040,
"Stash dev server health",
"Stash dev server is currently healthy on localhost:5173.",
"session_activity",
"stash-dev",
0.95,
Some(NOW - 3600),
),
(
1050,
"Ambiguous sidebar behavior",
"The sidebar behavior may belong to Stash or a generic browser tool.",
"discovery",
"stash-ui",
0.42,
None,
),
(
1060,
"Stash repo Codex approval UI",
"Stash repo implements Codex approval UI copy for its own settings screen.",
"discovery",
"stash-ui",
0.95,
None,
),
];
for (id, title, content, memory_type, topic_domain, confidence, expires_at_epoch) in memory_rows
{
conn.execute(
"INSERT INTO memories
(id, session_id, project, topic_key, title, content, memory_type,
created_at_epoch, updated_at_epoch, status, scope, source_project,
target_project, owner_scope, owner_key, topic_domain,
routing_confidence, context_class, expires_at_epoch)
VALUES
(?1, 'stash-session', ?2, ?3, ?4, ?5, ?6,
?7, ?8, 'active', 'project', ?2, ?2, 'repo', ?2, ?9, ?10,
'startup_core', ?11)",
params![
id,
STASH,
format!("topic-{id}"),
title,
content,
memory_type,
NOW - (2000 - id),
NOW - (2000 - id),
topic_domain,
confidence,
expires_at_epoch
],
)
.unwrap();
}
conn.execute(
"UPDATE memories
SET owner_scope = NULL, owner_key = NULL, routing_confidence = 0.42
WHERE id = 1050",
[],
)
.unwrap();
conn.execute(
"INSERT INTO memories
(id, session_id, project, topic_key, title, content, memory_type,
created_at_epoch, updated_at_epoch, status, scope, source_project,
target_project, owner_scope, owner_key, topic_domain,
routing_confidence, context_class)
VALUES
(1051, 'stash-session', ?1, 'topic-1051', 'Backfilled route',
'A v019 backfilled route should still be reviewed when confidence is missing.',
'discovery', ?2, ?2, 'active', 'project', ?1, ?1, 'repo', ?1,
'stash-ui', NULL, 'startup_core')",
params![STASH, NOW - 10],
)
.unwrap();
conn.execute(
"INSERT INTO memories
(id, session_id, project, topic_key, title, content, memory_type,
created_at_epoch, updated_at_epoch, status, scope, source_project,
target_project, owner_scope, owner_key, topic_domain,
routing_confidence, context_class)
VALUES
(1033, 'stash-session', ?1, 'topic-1033', 'Preference: global UI critique',
'Prefer direct UI critique for all product reviews.',
'preference', ?2, ?2, 'active', 'global', ?1, NULL, 'user',
'user:default', 'user-preference', 0.95, 'startup_core')",
params![STASH, NOW - 9],
)
.unwrap();
let workstream_rows = [
(2001, "Finish Stash drag-and-drop polish", "stash-ui", 0.95),
(2010, "Build Grok image API wrapper", "grok-api", 0.95),
(2011, "Fix Warp terminal startup routing", "macos", 0.95),
(2012, "Investigate Hermes MCP transport", "hermes", 0.95),
(2020, "Polish Stash sidebar interactions", "stash-ui", 0.95),
(2021, "Stash sidebar polish follow-up", "stash-ui", 0.95),
];
for (id, title, topic_domain, confidence) in workstream_rows {
conn.execute(
"INSERT INTO workstreams
(id, project, title, status, progress, created_at_epoch, updated_at_epoch,
source_project, target_project, owner_scope, owner_key, topic_domain,
routing_confidence, context_class)
VALUES (?1, ?2, ?3, 'active', 'in progress', ?4, ?5,
?2, ?2, 'repo', ?2, ?6, ?7, 'startup_core')",
params![
id,
STASH,
title,
NOW - (3000 - id),
NOW - (3000 - id),
topic_domain,
confidence
],
)
.unwrap();
}
}
fn refs(items: &[&str]) -> Vec<String> {
items.iter().map(|value| value.to_string()).collect()
}
fn has_ref(items: &[AuditItem], object_ref: &str) -> bool {
items.iter().any(|item| item.object_ref == object_ref)
}
#[test]
fn stash_pollution_audit_classifies_cleanup_buckets() -> Result<()> {
let conn = setup_conn();
seed_stash_pollution(&conn);
let report = audit_scope(
&conn,
&ScopeAuditRequest {
project: STASH,
limit: 100,
now_epoch: NOW,
},
)?;
assert!(has_ref(&report.likely_correct_repo_memory, "memory:1001"));
assert!(has_ref(&report.likely_correct_repo_memory, "memory:1002"));
assert!(has_ref(
&report.likely_correct_repo_memory,
"workstream:2001"
));
let pollution = &report.likely_cross_tool_domain_pollution;
assert!(has_ref(pollution, "memory:1010"));
assert!(has_ref(pollution, "memory:1011"));
assert!(has_ref(pollution, "memory:1020"));
assert!(has_ref(pollution, "memory:1021"));
assert!(has_ref(pollution, "workstream:2010"));
assert!(has_ref(pollution, "workstream:2011"));
assert!(has_ref(pollution, "workstream:2012"));
assert!(!has_ref(pollution, "memory:1060"));
let codex = pollution
.iter()
.find(|item| item.object_ref == "memory:1010")
.unwrap();
assert_eq!(codex.suggested_owner_scope.as_deref(), Some("tool"));
assert_eq!(codex.suggested_owner_key.as_deref(), Some("codex-cli"));
assert_eq!(codex.suggested_target_project, None);
assert!(report
.duplicate_preferences
.iter()
.any(|cluster| cluster.cluster_key == "ui-critique"
&& cluster.canonical_ref == "memory:1032"
&& cluster.refs.contains(&"memory:1030".to_string())
&& cluster.refs.contains(&"memory:1032".to_string())
&& !cluster.refs.contains(&"memory:1033".to_string())));
assert!(report
.duplicate_workstreams
.iter()
.any(|cluster| cluster.cluster_key == "stash-sidebar-polish"
&& cluster.canonical_ref == "workstream:2020"
&& cluster.refs.contains(&"workstream:2020".to_string())
&& cluster.refs.contains(&"workstream:2021".to_string())));
assert!(has_ref(&report.stale_temporal_facts, "memory:1040"));
assert!(has_ref(&report.low_confidence_routing, "memory:1050"));
assert!(has_ref(&report.low_confidence_routing, "memory:1051"));
assert!(has_ref(&report.low_confidence_routing, "memory:1060"));
Ok(())
}
#[test]
fn reroute_defaults_to_dry_run_and_confirm_preserves_provenance() -> Result<()> {
let conn = setup_conn();
seed_stash_pollution(&conn);
let parsed_refs = parse_object_refs(&refs(&["memory:1010,memory:1011"]))?;
let preview = reroute_objects(
&conn,
&RerouteRequest {
refs: &parsed_refs,
owner_scope: "tool",
owner_key: "codex-cli",
target_project: TargetProjectUpdate::Clear,
topic_domain: Some("codex-sandbox"),
context_class: Some("search_only"),
routing_confidence: Some(1.0),
reason: Some("Stash scope cleanup"),
dry_run: false,
confirm: false,
},
)?;
assert!(preview.dry_run);
assert_eq!(preview.affected.len(), 2);
let unchanged: (String, String, Option<String>, String) = conn.query_row(
"SELECT owner_scope, owner_key, target_project, status FROM memories WHERE id = 1010",
[],
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
)?;
assert_eq!(
unchanged,
(
"repo".to_string(),
STASH.to_string(),
Some(STASH.to_string()),
"active".to_string()
)
);
let event_count: i64 = conn.query_row(
"SELECT COUNT(*) FROM events WHERE event_type = 'scope_cleanup'",
[],
|row| row.get(0),
)?;
assert_eq!(event_count, 0);
let applied = reroute_objects(
&conn,
&RerouteRequest {
refs: &parsed_refs,
owner_scope: "tool",
owner_key: "codex-cli",
target_project: TargetProjectUpdate::Clear,
topic_domain: Some("codex-sandbox"),
context_class: Some("search_only"),
routing_confidence: Some(1.0),
reason: Some("Stash scope cleanup"),
dry_run: false,
confirm: true,
},
)?;
assert!(!applied.dry_run);
let routed: (String, String, Option<String>, String, String) = conn.query_row(
"SELECT owner_scope, owner_key, target_project, source_project, status
FROM memories WHERE id = 1010",
[],
|row| {
Ok((
row.get(0)?,
row.get(1)?,
row.get(2)?,
row.get(3)?,
row.get(4)?,
))
},
)?;
assert_eq!(
routed,
(
"tool".to_string(),
"codex-cli".to_string(),
None,
STASH.to_string(),
"active".to_string()
)
);
let event_count: i64 = conn.query_row(
"SELECT COUNT(*) FROM events WHERE event_type = 'scope_cleanup'",
[],
|row| row.get(0),
)?;
assert_eq!(event_count, 2);
Ok(())
}
#[test]
fn audit_limit_is_applied_per_bucket_after_classification() -> Result<()> {
let conn = setup_conn();
let now = NOW;
for id in 1..=8 {
conn.execute(
"INSERT INTO memories
(id, session_id, project, topic_key, title, content, memory_type,
created_at_epoch, updated_at_epoch, status, scope, source_project,
target_project, owner_scope, owner_key, topic_domain,
routing_confidence, context_class)
VALUES
(?1, 'limit-session', ?2, ?3, ?4, 'Stash repo UI memory.',
'discovery', ?5, ?5, 'active', 'project', ?2, ?2, 'repo',
?2, 'stash-ui', 0.95, 'startup_core')",
params![
id,
STASH,
format!("topic-limit-{id}"),
format!("Correct repo memory {id}"),
now + id
],
)?;
}
conn.execute(
"INSERT INTO memories
(id, session_id, project, topic_key, title, content, memory_type,
created_at_epoch, updated_at_epoch, status, scope, source_project,
target_project, owner_scope, owner_key, topic_domain,
routing_confidence, context_class)
VALUES
(99, 'limit-session', ?1, 'topic-old-codex', 'Codex sandbox',
'Codex CLI uses workspace-write sandbox approvals.', 'discovery',
?2, ?2, 'active', 'project', ?1, ?1, 'repo', ?1, 'codex-sandbox',
0.95, 'startup_core')",
params![STASH, now - 10_000],
)?;
let report = audit_scope(
&conn,
&ScopeAuditRequest {
project: STASH,
limit: 1,
now_epoch: now,
},
)?;
assert_eq!(report.likely_correct_repo_memory.len(), 1);
assert_eq!(report.likely_cross_tool_domain_pollution.len(), 1);
assert_eq!(
report.likely_cross_tool_domain_pollution[0].object_ref,
"memory:99"
);
Ok(())
}
#[test]
fn reroute_trims_owner_scope_and_key_before_persisting() -> Result<()> {
let conn = setup_conn();
seed_stash_pollution(&conn);
let parsed_refs = parse_object_refs(&refs(&["memory:1010"]))?;
reroute_objects(
&conn,
&RerouteRequest {
refs: &parsed_refs,
owner_scope: " tool ",
owner_key: " codex-cli ",
target_project: TargetProjectUpdate::Clear,
topic_domain: Some("codex-sandbox"),
context_class: None,
routing_confidence: Some(1.0),
reason: Some("trim owner fields"),
dry_run: false,
confirm: true,
},
)?;
let routed: (String, String, Option<String>, String) = conn.query_row(
"SELECT owner_scope, owner_key, target_project, context_class
FROM memories WHERE id = 1010",
[],
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
)?;
assert_eq!(
routed,
(
"tool".to_string(),
"codex-cli".to_string(),
None,
"search_only".to_string()
)
);
Ok(())
}
#[test]
fn archive_mixed_refs_does_not_hard_delete_and_rolls_back_on_invalid_ref() -> Result<()> {
let conn = setup_conn();
seed_stash_pollution(&conn);
let parsed_refs = parse_object_refs(&refs(&["memory:1040", "workstream:2010"]))?;
let preview = archive_objects(
&conn,
&ArchiveRequest {
refs: &parsed_refs,
reason: Some("not startup context"),
dry_run: false,
confirm: false,
},
)?;
assert!(preview.dry_run);
assert_eq!(
conn.query_row("SELECT status FROM memories WHERE id = 1040", [], |row| {
row.get::<_, String>(0)
})?,
"active"
);
let applied = archive_objects(
&conn,
&ArchiveRequest {
refs: &parsed_refs,
reason: Some("not startup context"),
dry_run: false,
confirm: true,
},
)?;
assert!(!applied.dry_run);
assert_eq!(
conn.query_row("SELECT status FROM memories WHERE id = 1040", [], |row| {
row.get::<_, String>(0)
})?,
"archived"
);
assert_eq!(
conn.query_row(
"SELECT status FROM workstreams WHERE id = 2010",
[],
|row| { row.get::<_, String>(0) }
)?,
"paused"
);
let remaining: i64 =
conn.query_row("SELECT COUNT(*) FROM memories WHERE id = 1040", [], |row| {
row.get(0)
})?;
assert_eq!(remaining, 1);
let conn = setup_conn();
seed_stash_pollution(&conn);
let bad_refs = parse_object_refs(&refs(&["memory:1040", "workstream:999"]))?;
let err = archive_objects(
&conn,
&ArchiveRequest {
refs: &bad_refs,
reason: Some("rollback test"),
dry_run: false,
confirm: true,
},
)
.expect_err("invalid mixed refs should fail");
assert!(err.to_string().contains("workstream:999 not found"));
assert_eq!(
conn.query_row("SELECT status FROM memories WHERE id = 1040", [], |row| {
row.get::<_, String>(0)
})?,
"active"
);
Ok(())
}
#[test]
fn merge_preferences_keeps_one_active_preference_with_merged_content() -> Result<()> {
let conn = setup_conn();
seed_stash_pollution(&conn);
let preview = merge_preferences(
&conn,
&MergePreferencesRequest {
project: STASH,
dry_run: false,
confirm: false,
},
)?;
assert!(preview.dry_run);
assert_eq!(preview.clusters.len(), 1);
assert_eq!(preview.clusters[0].canonical_ref, "memory:1032");
let merged = preview.clusters[0].merged_content.as_deref().unwrap();
assert!(merged.contains("direct UI critique"));
assert!(merged.contains("avoid decorative fluff"));
assert!(merged.contains("concise UI critique"));
assert_eq!(
conn.query_row("SELECT status FROM memories WHERE id = 1030", [], |row| {
row.get::<_, String>(0)
})?,
"active"
);
let applied = merge_preferences(
&conn,
&MergePreferencesRequest {
project: STASH,
dry_run: false,
confirm: true,
},
)?;
assert!(!applied.dry_run);
assert_eq!(applied.affected.len(), 3);
let canonical_mutation = applied
.affected
.iter()
.find(|mutation| mutation.object_ref == "memory:1032")
.expect("canonical mutation should be reported");
assert_eq!(
canonical_mutation.new_owner.owner_scope.as_deref(),
Some("repo")
);
let canonical: (String, String, String, String, String) = conn.query_row(
"SELECT status, content, owner_scope, owner_key, target_project
FROM memories WHERE id = 1032",
[],
|row| {
Ok((
row.get(0)?,
row.get(1)?,
row.get(2)?,
row.get(3)?,
row.get(4)?,
))
},
)?;
assert_eq!(canonical.0, "active");
assert!(canonical.1.contains("direct UI critique"));
assert!(canonical.1.contains("avoid decorative fluff"));
assert!(canonical.1.contains("concise UI critique"));
assert_eq!(canonical.2, "repo");
assert_eq!(canonical.3, STASH);
assert_eq!(canonical.4, STASH);
for id in [1030, 1031] {
assert_eq!(
conn.query_row(
"SELECT status, source_project FROM memories WHERE id = ?1",
params![id],
|row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
)?,
("stale".to_string(), STASH.to_string())
);
}
let global_pref: (String, String, Option<String>) = conn.query_row(
"SELECT status, owner_scope, target_project FROM memories WHERE id = 1033",
[],
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
)?;
assert_eq!(
global_pref,
("active".to_string(), "user".to_string(), None)
);
let prefs = memory::preference::query_project_preferences(&conn, STASH, 10)?;
let ui_prefs = prefs
.iter()
.filter(|pref| pref.text.contains("UI critique"))
.count();
assert_eq!(ui_prefs, 1);
let event_count: i64 = conn.query_row(
"SELECT COUNT(*) FROM events WHERE event_type = 'scope_cleanup'",
[],
|row| row.get(0),
)?;
assert_eq!(event_count, 3);
let duplicate_edges = conn
.prepare(
"SELECT from_memory_id, to_memory_id, edge_type
FROM memory_edges
WHERE edge_type = 'duplicates'
ORDER BY from_memory_id ASC",
)?
.query_map([], |row| {
Ok((
row.get::<_, i64>(0)?,
row.get::<_, i64>(1)?,
row.get::<_, String>(2)?,
))
})?
.collect::<std::result::Result<Vec<_>, _>>()?;
assert_eq!(
duplicate_edges,
vec![
(1030, 1032, "duplicates".to_string()),
(1031, 1032, "duplicates".to_string())
]
);
Ok(())
}
#[test]
fn object_ref_parser_requires_explicit_kind_and_dedupes() -> Result<()> {
let parsed = parse_object_refs(&refs(&["memory:1, workstream:2", "memory:1"]))?;
assert_eq!(
parsed,
vec![
ObjectRef::memory(1),
ObjectRef {
kind: ScopeObjectKind::Workstream,
id: 2
}
]
);
let err = parse_object_refs(&refs(&["1"])).expect_err("bare ids should fail");
assert!(err.to_string().contains("kind prefix"));
assert_eq!(
memory_refs_from_ids(&[3, 3, 4])?,
vec![ObjectRef::memory(3), ObjectRef::memory(4)]
);
Ok(())
}